@rizom/site-rizom-ai 0.2.0-alpha.259 → 0.2.0-alpha.261
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/dist/index.d.ts +122 -2
- package/dist/index.js +13231 -552
- package/dist/index.js.map +104 -18
- package/package.json +1 -1
- package/src/asked-datasource.ts +72 -0
- package/src/asked.tsx +198 -0
- package/src/opening.tsx +12 -2
- package/src/routes.ts +4 -1
- package/src/site.ts +22 -5
- package/src/story/living-organism.tsx +10 -0
- package/src/story/runtime.ts +194 -1
- package/src/story.css +308 -8
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseDataSourceContext,
|
|
3
|
+
DataSource,
|
|
4
|
+
DataSourceSchema,
|
|
5
|
+
IShell,
|
|
6
|
+
} from "@brains/plugins";
|
|
7
|
+
import { proximityMapDataSchema } from "@brains/agent-discovery/proximity-map";
|
|
8
|
+
import { askedSchema } from "./asked";
|
|
9
|
+
|
|
10
|
+
/** How many kept questions the chapter shows, most asked first. */
|
|
11
|
+
const LIMIT = 12;
|
|
12
|
+
const MAP_SOURCE_ID = "agent-discovery:proximity-map";
|
|
13
|
+
const faqList = askedSchema.pick({ faqs: true });
|
|
14
|
+
const emptyMap = {
|
|
15
|
+
center: { kind: "identity" },
|
|
16
|
+
nodes: [],
|
|
17
|
+
clusters: [],
|
|
18
|
+
sightings: [],
|
|
19
|
+
distanceRange: { min: 0, max: 0 },
|
|
20
|
+
pendingCount: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The "Asked before" chapter's data: the published FAQs, read through the faq
|
|
25
|
+
* plugin's own datasource so drafts stay out the way its own lists keep them
|
|
26
|
+
* out, and so this site carries no FAQ runtime. None where the brain keeps
|
|
27
|
+
* no FAQs, and the chapter renders nothing.
|
|
28
|
+
*/
|
|
29
|
+
export interface AskedSources {
|
|
30
|
+
faqs: () => DataSource | undefined;
|
|
31
|
+
/** The live map's datasource, registered by agent-discovery; absent when the plugin is not loaded. */
|
|
32
|
+
map: () => DataSource | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RizomAskedDataSource implements DataSource {
|
|
36
|
+
public readonly id = "rizom:asked";
|
|
37
|
+
public readonly name = "Rizom Asked DataSource";
|
|
38
|
+
public readonly description =
|
|
39
|
+
"The published FAQs, with their sources, over the live network for the Asked-before chapter";
|
|
40
|
+
private readonly sources: AskedSources;
|
|
41
|
+
|
|
42
|
+
constructor(sources: AskedSources) {
|
|
43
|
+
this.sources = sources;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetch<T>(
|
|
47
|
+
query: unknown,
|
|
48
|
+
outputSchema: DataSourceSchema<T>,
|
|
49
|
+
context: BaseDataSourceContext,
|
|
50
|
+
): Promise<T> {
|
|
51
|
+
const faqs = this.sources.faqs();
|
|
52
|
+
const map = this.sources.map();
|
|
53
|
+
const [kept, network] = await Promise.all([
|
|
54
|
+
faqs?.fetch
|
|
55
|
+
? faqs.fetch({ query: { limit: LIMIT } }, faqList, context)
|
|
56
|
+
: faqList.parse({ faqs: [] }),
|
|
57
|
+
map?.fetch
|
|
58
|
+
? map.fetch(query, proximityMapDataSchema, context)
|
|
59
|
+
: proximityMapDataSchema.parse(emptyMap),
|
|
60
|
+
]);
|
|
61
|
+
return outputSchema.parse({ ...network, faqs: kept.faqs });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The datasource on the brain's shell, built when the site's plugin registers. */
|
|
66
|
+
export function askedDataSource(shell: IShell): DataSource {
|
|
67
|
+
const registry = shell.getDataSourceRegistry();
|
|
68
|
+
return new RizomAskedDataSource({
|
|
69
|
+
faqs: () => registry.get("faq:entities"),
|
|
70
|
+
map: () => registry.get(MAP_SOURCE_ID),
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/asked.tsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
import { proximityMapDataSchema } from "@brains/agent-discovery/proximity-map";
|
|
4
|
+
import { answeredBy } from "@brains/contracts";
|
|
5
|
+
import { StructuredContentFormatter } from "@brains/content-formatters";
|
|
6
|
+
import { createTemplate, type Template } from "@brains/templates";
|
|
7
|
+
import { MarkdownContent } from "@brains/ui-library";
|
|
8
|
+
import { z } from "@rizom/site";
|
|
9
|
+
import { NetworkLayer } from "./opening";
|
|
10
|
+
import { placeNetwork } from "./story/network";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* "Asked before": visitors' questions the owner kept and published, most
|
|
14
|
+
* asked first, each opening on the answer, who answered, and the sources in
|
|
15
|
+
* their owners' words. The chapter follows "Where this goes" and precedes the
|
|
16
|
+
* doors: the reader has seen the argument and has their own doubts before
|
|
17
|
+
* being asked to choose. Opening a question lights the brains its answer
|
|
18
|
+
* drew on in the live drawing (see ./story/runtime); counts sort the list
|
|
19
|
+
* and are never shown.
|
|
20
|
+
*/
|
|
21
|
+
type AskedSourceSchema = z.ZodObject<{
|
|
22
|
+
id: z.ZodString;
|
|
23
|
+
title: z.ZodString;
|
|
24
|
+
url: z.ZodNullable<z.ZodString>;
|
|
25
|
+
excerpt: z.ZodNullable<z.ZodString>;
|
|
26
|
+
brain: z.ZodNullable<
|
|
27
|
+
z.ZodObject<{ name: z.ZodString; url: z.ZodNullable<z.ZodString> }>
|
|
28
|
+
>;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
const askedSourceSchema: AskedSourceSchema = z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
title: z.string(),
|
|
34
|
+
url: z.string().nullable(),
|
|
35
|
+
excerpt: z.string().nullable(),
|
|
36
|
+
brain: z.object({ name: z.string(), url: z.string().nullable() }).nullable(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const askedFaqSchema: z.ZodObject<{
|
|
40
|
+
id: z.ZodString;
|
|
41
|
+
question: z.ZodString;
|
|
42
|
+
answer: z.ZodString;
|
|
43
|
+
asked: z.ZodNumber;
|
|
44
|
+
sources: z.ZodArray<AskedSourceSchema>;
|
|
45
|
+
}> = z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
question: z.string(),
|
|
48
|
+
answer: z.string(),
|
|
49
|
+
asked: z.number().int(),
|
|
50
|
+
sources: z.array(askedSourceSchema),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** The chapter's own words, authored as a content section like every chapter's. */
|
|
54
|
+
type AskedCopySchema = z.ZodObject<{
|
|
55
|
+
cap: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
claim: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
body: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
export const askedCopySchema: AskedCopySchema = z.object({
|
|
61
|
+
/** The eyebrow. */
|
|
62
|
+
cap: z.string().nullable().default(null),
|
|
63
|
+
/** The heading. */
|
|
64
|
+
claim: z.string().nullable().default(null),
|
|
65
|
+
/** The line under the heading. */
|
|
66
|
+
body: z.string().nullable().default(null),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const DEFAULT_COPY = {
|
|
70
|
+
cap: "Asked before",
|
|
71
|
+
claim: "What people ask the network",
|
|
72
|
+
body: "Questions visitors put to Rizom, answered from the connected brains and kept by the owner. Open one: the brains that answered light up.",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The published FAQs over the live network, which the chapter draws beside them. */
|
|
76
|
+
export const askedSchema: z.ZodObject<
|
|
77
|
+
(typeof proximityMapDataSchema)["shape"] &
|
|
78
|
+
AskedCopySchema["shape"] & {
|
|
79
|
+
faqs: z.ZodArray<typeof askedFaqSchema>;
|
|
80
|
+
}
|
|
81
|
+
> = proximityMapDataSchema.extend({
|
|
82
|
+
...askedCopySchema.shape,
|
|
83
|
+
faqs: z.array(askedFaqSchema),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export type AskedData = z.output<typeof askedSchema>;
|
|
87
|
+
type AskedSource = z.output<typeof askedSourceSchema>;
|
|
88
|
+
|
|
89
|
+
const OWNER = "Rizom";
|
|
90
|
+
|
|
91
|
+
/** What the drawing needs of a kept source: its key, title and brain. */
|
|
92
|
+
function forDrawing(sources: AskedSource[]): string {
|
|
93
|
+
return JSON.stringify(
|
|
94
|
+
sources.map((source) => ({
|
|
95
|
+
id: source.id,
|
|
96
|
+
title: source.title,
|
|
97
|
+
...(source.brain
|
|
98
|
+
? {
|
|
99
|
+
brain: {
|
|
100
|
+
name: source.brain.name,
|
|
101
|
+
...(source.brain.url ? { url: source.brain.url } : {}),
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
})),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function Sources({ sources }: { sources: AskedSource[] }): JSX.Element | null {
|
|
110
|
+
if (sources.length === 0) return null;
|
|
111
|
+
const brains = sources.flatMap((source) =>
|
|
112
|
+
source.brain ? [source.brain.name] : [],
|
|
113
|
+
);
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
{brains.length > 0 && (
|
|
117
|
+
<p className="asked__by">{answeredBy(OWNER, brains)}</p>
|
|
118
|
+
)}
|
|
119
|
+
<ul className="asked__sources" aria-label="Sources">
|
|
120
|
+
{sources.map((source) => (
|
|
121
|
+
<li
|
|
122
|
+
key={source.id}
|
|
123
|
+
data-ask-source={source.id}
|
|
124
|
+
{...(source.brain ? { "data-ask-brain": source.brain.name } : {})}
|
|
125
|
+
>
|
|
126
|
+
<span className="asked__mark" aria-hidden="true" />
|
|
127
|
+
<span className="asked__piece">
|
|
128
|
+
{source.brain && <b>{source.brain.name}</b>}
|
|
129
|
+
{source.url ? (
|
|
130
|
+
<a href={source.url} target="_blank" rel="noopener noreferrer">
|
|
131
|
+
{source.title}
|
|
132
|
+
</a>
|
|
133
|
+
) : (
|
|
134
|
+
<span>{source.title}</span>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
{source.excerpt && <q>{source.excerpt}</q>}
|
|
138
|
+
</li>
|
|
139
|
+
))}
|
|
140
|
+
</ul>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function Asked(data: AskedData): JSX.Element {
|
|
146
|
+
const { faqs } = data;
|
|
147
|
+
if (faqs.length === 0) return <></>;
|
|
148
|
+
return (
|
|
149
|
+
<section
|
|
150
|
+
id="asked"
|
|
151
|
+
className="chapter asked"
|
|
152
|
+
data-title="Asked before"
|
|
153
|
+
data-lights-network=""
|
|
154
|
+
>
|
|
155
|
+
<p className="eyebrow">{data.cap ?? DEFAULT_COPY.cap}</p>
|
|
156
|
+
<h2>{data.claim ?? DEFAULT_COPY.claim}</h2>
|
|
157
|
+
<p>{data.body ?? DEFAULT_COPY.body}</p>
|
|
158
|
+
<NetworkLayer {...placeNetwork(data)} />
|
|
159
|
+
<div className="asked__list">
|
|
160
|
+
{faqs.map((faq) => (
|
|
161
|
+
<details
|
|
162
|
+
key={faq.id}
|
|
163
|
+
name="asked"
|
|
164
|
+
data-ask-answer={forDrawing(faq.sources)}
|
|
165
|
+
>
|
|
166
|
+
<summary>{faq.question}</summary>
|
|
167
|
+
<div className="asked__answer">
|
|
168
|
+
<MarkdownContent markdown={faq.answer} />
|
|
169
|
+
<Sources sources={faq.sources} />
|
|
170
|
+
</div>
|
|
171
|
+
</details>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The chapter's words, edited as an ordinary markdown section and spliced
|
|
179
|
+
// over the live FAQs by the content overlay.
|
|
180
|
+
const askedCopyFormatter = new StructuredContentFormatter(askedCopySchema, {
|
|
181
|
+
title: "Asked before",
|
|
182
|
+
mappings: [
|
|
183
|
+
{ key: "cap", label: "Cap", type: "string" },
|
|
184
|
+
{ key: "claim", label: "Claim", type: "string" },
|
|
185
|
+
{ key: "body", label: "Body", type: "string" },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export const askedTemplate: Template = createTemplate({
|
|
190
|
+
name: "asked",
|
|
191
|
+
description:
|
|
192
|
+
"Asked before: the visitors' questions the owner kept, with their answers and sources",
|
|
193
|
+
schema: askedSchema,
|
|
194
|
+
dataSourceId: "rizom:asked",
|
|
195
|
+
overlayFormatter: askedCopyFormatter,
|
|
196
|
+
requiredPermission: "public",
|
|
197
|
+
layout: { component: Asked },
|
|
198
|
+
});
|
package/src/opening.tsx
CHANGED
|
@@ -45,7 +45,7 @@ const PULSE_TRAVEL = 4.55;
|
|
|
45
45
|
const replyDelay = (reach: number, after = 0): string =>
|
|
46
46
|
`${(reach * PULSE_TRAVEL + after).toFixed(2)}s`;
|
|
47
47
|
|
|
48
|
-
function NetworkLayer({
|
|
48
|
+
export function NetworkLayer({
|
|
49
49
|
brains,
|
|
50
50
|
kin,
|
|
51
51
|
}: ReturnType<typeof placeNetwork>): JSX.Element | null {
|
|
@@ -75,6 +75,7 @@ function NetworkLayer({
|
|
|
75
75
|
<line
|
|
76
76
|
key={`thread-${brain.id}`}
|
|
77
77
|
className="net-thread"
|
|
78
|
+
data-brain={brain.id}
|
|
78
79
|
x1="50"
|
|
79
80
|
y1="50"
|
|
80
81
|
x2={brain.x}
|
|
@@ -83,7 +84,11 @@ function NetworkLayer({
|
|
|
83
84
|
))}
|
|
84
85
|
<circle className="net-pulse" cx="50" cy="50" r="46" />
|
|
85
86
|
{brains.map((brain) => (
|
|
86
|
-
<g
|
|
87
|
+
<g
|
|
88
|
+
key={`reply-${brain.id}`}
|
|
89
|
+
className="net-reply"
|
|
90
|
+
data-brain={brain.id}
|
|
91
|
+
>
|
|
87
92
|
<circle
|
|
88
93
|
className="net-echo"
|
|
89
94
|
cx={brain.x}
|
|
@@ -119,6 +124,7 @@ function NetworkLayer({
|
|
|
119
124
|
<li
|
|
120
125
|
key={brain.id}
|
|
121
126
|
className="net-mark"
|
|
127
|
+
data-brain={brain.id}
|
|
122
128
|
style={{ left: `${brain.x}%`, top: `${brain.y}%` }}
|
|
123
129
|
>
|
|
124
130
|
<a href={`/agents/${brain.id}`} aria-label={brain.name}>
|
|
@@ -132,6 +138,7 @@ function NetworkLayer({
|
|
|
132
138
|
<li
|
|
133
139
|
key={brain.id}
|
|
134
140
|
className={`net-name net-name--${brain.side}`}
|
|
141
|
+
data-brain={brain.id}
|
|
135
142
|
style={{ left: `${brain.x}%`, top: `${brain.y}%` }}
|
|
136
143
|
>
|
|
137
144
|
{brain.name}
|
|
@@ -183,6 +190,9 @@ export function Opening(data: OpeningData): JSX.Element {
|
|
|
183
190
|
)
|
|
184
191
|
)}
|
|
185
192
|
<NetworkLayer {...placeNetwork(data)} />
|
|
193
|
+
{data.askBox && (
|
|
194
|
+
<svg className="net-leads" data-net-leads="" aria-hidden="true" />
|
|
195
|
+
)}
|
|
186
196
|
</section>
|
|
187
197
|
);
|
|
188
198
|
}
|
package/src/routes.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const aiRoutes: RouteDefinitionInput[] = [
|
|
|
8
8
|
{
|
|
9
9
|
// The homepage, told as a story: the opening is the live network under the
|
|
10
10
|
// authored words with the Ask box, then the science, the shift, the
|
|
11
|
-
// organism, where this goes and the two ways in. The problem, the system
|
|
11
|
+
// organism, where this goes, what was asked before and the two ways in. The problem, the system
|
|
12
12
|
// and the proof are told on /work, /brain and by the opening itself. Copy
|
|
13
13
|
// for every section belongs to the separate content repo under
|
|
14
14
|
// site-content/living-memory/; keep that content identity stable.
|
|
@@ -25,6 +25,9 @@ export const aiRoutes: RouteDefinitionInput[] = [
|
|
|
25
25
|
{ id: "turn", template: "living-memory:turn" },
|
|
26
26
|
{ id: "growth", template: "living-memory:growth" },
|
|
27
27
|
{ id: "arc", template: "living-memory:arc" },
|
|
28
|
+
// What visitors asked before, answered and kept by the owner, read
|
|
29
|
+
// live from the published FAQs (see ./asked-datasource).
|
|
30
|
+
{ id: "asked", template: "rizom:asked", dataQuery: {} },
|
|
28
31
|
{ id: "doors", template: "living-memory:doors" },
|
|
29
32
|
],
|
|
30
33
|
},
|
package/src/site.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { livingMemorySections } from "./living-memory";
|
|
|
4
4
|
import { brainSections } from "./brain";
|
|
5
5
|
import { openingTemplate } from "./opening";
|
|
6
6
|
import { openingDataSource } from "./opening-datasource";
|
|
7
|
+
import { askedTemplate } from "./asked";
|
|
8
|
+
import { askedDataSource } from "./asked-datasource";
|
|
7
9
|
import { writingTemplate } from "./writing";
|
|
8
10
|
import { writingDataSource } from "./writing-datasource";
|
|
9
11
|
import { workSections } from "./work";
|
|
@@ -19,8 +21,16 @@ export const rizomAiSite: SitePackage = createRizomSite({
|
|
|
19
21
|
routes: aiRoutes,
|
|
20
22
|
runtime: {
|
|
21
23
|
contentNamespace: "rizom",
|
|
22
|
-
templates: {
|
|
23
|
-
|
|
24
|
+
templates: {
|
|
25
|
+
opening: openingTemplate,
|
|
26
|
+
writing: writingTemplate,
|
|
27
|
+
asked: askedTemplate,
|
|
28
|
+
},
|
|
29
|
+
dataSourceFactories: [
|
|
30
|
+
openingDataSource,
|
|
31
|
+
writingDataSource,
|
|
32
|
+
askedDataSource,
|
|
33
|
+
],
|
|
24
34
|
},
|
|
25
35
|
// Every page is authored schema-first (see ./living-memory, ./brain,
|
|
26
36
|
// ./work, ./foundation).
|
|
@@ -35,10 +45,17 @@ export const rizomAiSite: SitePackage = createRizomSite({
|
|
|
35
45
|
// datasource (see ./writing-datasource); entityDisplay supplies the labels
|
|
36
46
|
// and detail-page paths the site builder links each piece by. Navigation is hidden — the layout's bar owns the nav, so the
|
|
37
47
|
// auto-generated per-type indexes stay unlinked.
|
|
48
|
+
// What a visitor's answer may cite: the site's own pieces and the
|
|
49
|
+
// network's, which live on their brains' sites and have no route here.
|
|
38
50
|
entityDisplay: {
|
|
39
|
-
post: { label: "Essay", navigation: { show: false } },
|
|
40
|
-
deck: { label: "Talk", navigation: { show: false } },
|
|
41
|
-
agent: { label: "Agent", navigation: { show: false } },
|
|
51
|
+
post: { label: "Essay", citable: true, navigation: { show: false } },
|
|
52
|
+
deck: { label: "Talk", citable: true, navigation: { show: false } },
|
|
53
|
+
agent: { label: "Agent", citable: true, navigation: { show: false } },
|
|
54
|
+
"network-piece": {
|
|
55
|
+
label: "From the network",
|
|
56
|
+
citable: true,
|
|
57
|
+
navigation: { show: false },
|
|
58
|
+
},
|
|
42
59
|
},
|
|
43
60
|
});
|
|
44
61
|
|
|
@@ -112,6 +112,16 @@ export const livingOrganism: Organism = organism({
|
|
|
112
112
|
economy.map((p, i) => [`E${i + 1}`, at(p, i % 3 === 0 ? 6 : 4)]),
|
|
113
113
|
),
|
|
114
114
|
},
|
|
115
|
+
// Asked before: the economy stays, at rest; the live network beside the
|
|
116
|
+
// chapter lights from the open question (see ./runtime).
|
|
117
|
+
{
|
|
118
|
+
...Object.fromEntries(
|
|
119
|
+
Object.entries(one).map(([k, v]) => [k, shrink(v)]),
|
|
120
|
+
),
|
|
121
|
+
...Object.fromEntries(
|
|
122
|
+
economy.map((p, i) => [`E${i + 1}`, at(p, i % 3 === 0 ? 6 : 4)]),
|
|
123
|
+
),
|
|
124
|
+
},
|
|
115
125
|
// Two ways in: back to one lantern.
|
|
116
126
|
{ L: [C, C, 13, 1] },
|
|
117
127
|
],
|
package/src/story/runtime.ts
CHANGED
|
@@ -89,6 +89,8 @@ export const storyRuntimeScript: string = `(function () {
|
|
|
89
89
|
var stage = currentChapter(tops, line, stageCount);
|
|
90
90
|
if (figure) figure.dataset.stage = String(stage);
|
|
91
91
|
chapters.forEach(function (chapter, i) { chapter.classList.toggle("is-current", i === current); });
|
|
92
|
+
// A chapter that lights the network keeps the live drawing in view while it is read.
|
|
93
|
+
story.classList.toggle("is-asked", !!(chapters[current] && chapters[current].hasAttribute("data-lights-network")));
|
|
92
94
|
if (!rail) return;
|
|
93
95
|
var max = document.documentElement.scrollHeight - innerHeight;
|
|
94
96
|
var progress = max > 0 ? Math.min(1, scrollY / max) : 0;
|
|
@@ -107,10 +109,201 @@ export const storyRuntimeScript: string = `(function () {
|
|
|
107
109
|
addEventListener("resize", function () { placeRail(); read(); });
|
|
108
110
|
addEventListener("load", function () { placeRail(); read(); });
|
|
109
111
|
}
|
|
112
|
+
// The homepage's drawing listens to the Ask box: an answer's sources name
|
|
113
|
+
// the brains whose published memory they came from (the box's ask:sources
|
|
114
|
+
// event), and those brains light while the rest dim. A source without a
|
|
115
|
+
// brain is this brain's own and lights the center. Pointing works both
|
|
116
|
+
// ways: a listed source lights its brain, a brain flags its listed sources.
|
|
117
|
+
function listen() {
|
|
118
|
+
// The opening draws the network, and a chapter may draw it again beside
|
|
119
|
+
// its own words: every drawing answers the same events.
|
|
120
|
+
var layers = Array.prototype.slice.call(document.querySelectorAll(".net-layer"));
|
|
121
|
+
if (!layers.length) return;
|
|
122
|
+
function all(selector) {
|
|
123
|
+
return layers.reduce(function (found, layer) { return found.concat(Array.prototype.slice.call(layer.querySelectorAll(selector))); }, []);
|
|
124
|
+
}
|
|
125
|
+
var marks = all(".net-mark[data-brain]");
|
|
126
|
+
var threads = all(".net-thread[data-brain]");
|
|
127
|
+
var replies = all(".net-reply[data-brain]");
|
|
128
|
+
var names = all(".net-name[data-brain]");
|
|
129
|
+
var sourceBrain = {};
|
|
130
|
+
function brainOf(source) {
|
|
131
|
+
var brain = source && source.brain;
|
|
132
|
+
if (!brain) return "";
|
|
133
|
+
var host = "";
|
|
134
|
+
if (brain.url) { try { host = new URL(brain.url).host; } catch (e) { host = ""; } }
|
|
135
|
+
var byHost = host && marks.filter(function (m) { return m.getAttribute("data-brain") === host; })[0];
|
|
136
|
+
if (byHost) return host;
|
|
137
|
+
var name = String(brain.name || "").toLowerCase();
|
|
138
|
+
var byName = marks.filter(function (m) {
|
|
139
|
+
var a = m.querySelector("[aria-label]");
|
|
140
|
+
return (a ? a.getAttribute("aria-label") : "").toLowerCase() === name;
|
|
141
|
+
})[0];
|
|
142
|
+
return byName ? byName.getAttribute("data-brain") : "";
|
|
143
|
+
}
|
|
144
|
+
function light(id, on) {
|
|
145
|
+
[marks, threads, replies, names].forEach(function (list) {
|
|
146
|
+
list.forEach(function (el) { if (el.getAttribute("data-brain") === id) el.classList.toggle("is-lit", on); });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function hot(id, on) {
|
|
150
|
+
marks.concat(threads).forEach(function (el) { if (el.getAttribute("data-brain") === id) el.classList.toggle("is-hot", on); });
|
|
151
|
+
}
|
|
152
|
+
document.addEventListener("ask:sources", function (event) {
|
|
153
|
+
var detail = event.detail || {};
|
|
154
|
+
var sources = Array.isArray(detail.sources) ? detail.sources : [];
|
|
155
|
+
var lit = {};
|
|
156
|
+
var rizom = false;
|
|
157
|
+
sourceBrain = {};
|
|
158
|
+
sources.forEach(function (source) {
|
|
159
|
+
var id = brainOf(source);
|
|
160
|
+
if (id) { lit[id] = true; sourceBrain[source.id] = id; } else rizom = true;
|
|
161
|
+
});
|
|
162
|
+
marks.forEach(function (m) { light(m.getAttribute("data-brain"), !!lit[m.getAttribute("data-brain")]); });
|
|
163
|
+
layers.forEach(function (layer) {
|
|
164
|
+
layer.classList.toggle("has-replies", Object.keys(lit).length > 0);
|
|
165
|
+
layer.classList.toggle("is-rizom", rizom && sources.length > 0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
function listed(target) {
|
|
169
|
+
var row = target && target.closest ? target.closest("[data-ask-source]") : null;
|
|
170
|
+
return row ? row : null;
|
|
171
|
+
}
|
|
172
|
+
function point(event, on) {
|
|
173
|
+
var row = listed(event.target);
|
|
174
|
+
if (row) { var id = sourceBrain[row.getAttribute("data-ask-source")]; if (id) hot(id, on); return; }
|
|
175
|
+
var mark = event.target && event.target.closest ? event.target.closest(".net-mark[data-brain]") : null;
|
|
176
|
+
if (!mark) return;
|
|
177
|
+
var brain = mark.getAttribute("data-brain");
|
|
178
|
+
hot(brain, on);
|
|
179
|
+
Object.keys(sourceBrain).forEach(function (key) {
|
|
180
|
+
if (sourceBrain[key] !== brain) return;
|
|
181
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-ask-source]"), function (row) {
|
|
182
|
+
if (row.getAttribute("data-ask-source") !== key) return;
|
|
183
|
+
if (on) row.setAttribute("data-ask-hot", ""); else row.removeAttribute("data-ask-hot");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// "Asked before": an open question's kept sources light the drawing the
|
|
188
|
+
// way a fresh answer's do; with no question open, the drawing rests.
|
|
189
|
+
function askedSources() {
|
|
190
|
+
var open = document.querySelector("details[data-ask-answer][open]");
|
|
191
|
+
if (!open) return [];
|
|
192
|
+
try { var kept = JSON.parse(open.getAttribute("data-ask-answer") || "[]"); return Array.isArray(kept) ? kept : []; } catch (e) { return []; }
|
|
193
|
+
}
|
|
194
|
+
document.addEventListener("toggle", function (event) {
|
|
195
|
+
var target = event.target;
|
|
196
|
+
if (!target || !target.hasAttribute || !target.hasAttribute("data-ask-answer")) return;
|
|
197
|
+
document.dispatchEvent(new CustomEvent("ask:sources", { detail: { sources: askedSources() } }));
|
|
198
|
+
}, true);
|
|
199
|
+
// The drawing belongs to the page while an answer is open. On desktop a
|
|
200
|
+
// dotted lead runs from each listed source to its brain in the opening's
|
|
201
|
+
// drawing; on a phone the drawing joins the open conversation as the
|
|
202
|
+
// first item of its scroll, and a tap on a lit dot brings its source
|
|
203
|
+
// into view. Only the opening's drawing is lent; a chapter's stays put.
|
|
204
|
+
var home = layers[0];
|
|
205
|
+
var homeMarks = marks.filter(function (m) { return home.contains(m); });
|
|
206
|
+
var leadsLayer = document.querySelector("[data-net-leads]");
|
|
207
|
+
var narrow = typeof matchMedia === "function" ? matchMedia("(max-width: 60rem)") : { matches: false };
|
|
208
|
+
var askHost = document.querySelector("[data-ask-box]");
|
|
209
|
+
var SVG = "http://www.w3.org/2000/svg", GAP = 6, GLYPH = 9;
|
|
210
|
+
function listedRow(scope, key) {
|
|
211
|
+
if (!scope) return null;
|
|
212
|
+
var rows = Array.prototype.filter.call(scope.querySelectorAll("[data-ask-source]"), function (row) {
|
|
213
|
+
return row.getAttribute("data-ask-source") === key;
|
|
214
|
+
});
|
|
215
|
+
return rows.length ? rows[rows.length - 1] : null;
|
|
216
|
+
}
|
|
217
|
+
function drawLeads() {
|
|
218
|
+
if (!leadsLayer) return;
|
|
219
|
+
while (leadsLayer.firstChild) leadsLayer.removeChild(leadsLayer.firstChild);
|
|
220
|
+
if (narrow.matches || !askHost) return;
|
|
221
|
+
// The layer's own box is the frame: coordinates are its, whatever positions it.
|
|
222
|
+
var frame = leadsLayer.getBoundingClientRect();
|
|
223
|
+
leadsLayer.setAttribute("viewBox", "0 0 " + frame.width + " " + frame.height);
|
|
224
|
+
Object.keys(sourceBrain).forEach(function (key) {
|
|
225
|
+
var mark = homeMarks.filter(function (m) { return m.getAttribute("data-brain") === sourceBrain[key]; })[0];
|
|
226
|
+
var row = listedRow(askHost, key);
|
|
227
|
+
if (!mark || !row) return;
|
|
228
|
+
var from = row.getBoundingClientRect();
|
|
229
|
+
if (!from.height) return;
|
|
230
|
+
var to = mark.getBoundingClientRect();
|
|
231
|
+
var x1 = from.right + GAP - frame.left;
|
|
232
|
+
var y1 = from.top + from.height / 2 - frame.top;
|
|
233
|
+
var toward = to.left + to.width / 2 - frame.left;
|
|
234
|
+
var x2 = toward > x1 ? toward - GLYPH : toward + GLYPH;
|
|
235
|
+
var y2 = to.top + to.height / 2 - frame.top;
|
|
236
|
+
var bend = Math.max(40, Math.abs(x2 - x1) / 2);
|
|
237
|
+
var lead = document.createElementNS(SVG, "path");
|
|
238
|
+
lead.setAttribute("data-lead", key);
|
|
239
|
+
lead.setAttribute("d", "M" + x1 + " " + y1 + " C" + (x1 + bend) + " " + y1 + " " + (x2 - bend) + " " + y2 + " " + x2 + " " + y2);
|
|
240
|
+
leadsLayer.appendChild(lead);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
var leadsPending = false;
|
|
244
|
+
function scheduleLeads() {
|
|
245
|
+
if (leadsPending || !leadsLayer) return;
|
|
246
|
+
leadsPending = true;
|
|
247
|
+
requestAnimationFrame(function () { leadsPending = false; drawLeads(); });
|
|
248
|
+
}
|
|
249
|
+
if (leadsLayer) {
|
|
250
|
+
addEventListener("resize", scheduleLeads);
|
|
251
|
+
// Scrolling the page or the conversation moves the listed sources.
|
|
252
|
+
document.addEventListener("scroll", scheduleLeads, true);
|
|
253
|
+
document.addEventListener("toggle", scheduleLeads, true);
|
|
254
|
+
if (askHost && typeof ResizeObserver === "function") new ResizeObserver(scheduleLeads).observe(askHost);
|
|
255
|
+
}
|
|
256
|
+
document.addEventListener("ask:sources", function () { drawLeads(); });
|
|
257
|
+
function sheetOpen() { return !!askHost && askHost.hasAttribute("data-ask-sheet"); }
|
|
258
|
+
var slot = null;
|
|
259
|
+
function lend() {
|
|
260
|
+
var dock = askHost.querySelector("[data-ask-dock]");
|
|
261
|
+
if (!home || !dock || home.parentElement === dock) return;
|
|
262
|
+
slot = document.createElement("div");
|
|
263
|
+
slot.className = "net-slot";
|
|
264
|
+
slot.setAttribute("aria-hidden", "true");
|
|
265
|
+
home.parentNode.insertBefore(slot, home);
|
|
266
|
+
dock.appendChild(home);
|
|
267
|
+
}
|
|
268
|
+
function giveBack() {
|
|
269
|
+
if (!slot) return;
|
|
270
|
+
slot.parentNode.insertBefore(home, slot);
|
|
271
|
+
slot.parentNode.removeChild(slot);
|
|
272
|
+
slot = null;
|
|
273
|
+
}
|
|
274
|
+
if (askHost && home && typeof MutationObserver === "function") {
|
|
275
|
+
// The box mounts, and so its dock appears, after the sheet has opened.
|
|
276
|
+
new MutationObserver(function () { if (sheetOpen()) lend(); else giveBack(); })
|
|
277
|
+
.observe(askHost, { attributes: true, attributeFilter: ["data-ask-sheet"], childList: true, subtree: true });
|
|
278
|
+
}
|
|
279
|
+
document.addEventListener("click", function (event) {
|
|
280
|
+
if (!sheetOpen() || !home) return;
|
|
281
|
+
var mark = event.target && event.target.closest ? event.target.closest(".net-mark[data-brain]") : null;
|
|
282
|
+
if (!mark || !home.contains(mark)) return;
|
|
283
|
+
var brain = mark.getAttribute("data-brain");
|
|
284
|
+
var key = Object.keys(sourceBrain).filter(function (k) { return sourceBrain[k] === brain; })[0];
|
|
285
|
+
var area = askHost.querySelector(".brain-box-scroll");
|
|
286
|
+
var row = key ? listedRow(area, key) : null;
|
|
287
|
+
if (!row || !area) return;
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
var at = row.getBoundingClientRect(), box = area.getBoundingClientRect();
|
|
290
|
+
var top = area.scrollTop + at.top - box.top - area.clientHeight / 2 + at.height / 2;
|
|
291
|
+
if (area.scrollTo) area.scrollTo({ top: top, behavior: "smooth" }); else area.scrollTop = top;
|
|
292
|
+
row.removeAttribute("data-ask-flash");
|
|
293
|
+
void row.offsetWidth;
|
|
294
|
+
row.setAttribute("data-ask-flash", "");
|
|
295
|
+
setTimeout(function () { row.removeAttribute("data-ask-flash"); }, 1500);
|
|
296
|
+
}, true);
|
|
297
|
+
document.addEventListener("mouseover", function (e) { point(e, true); });
|
|
298
|
+
document.addEventListener("mouseout", function (e) { point(e, false); });
|
|
299
|
+
document.addEventListener("focusin", function (e) { point(e, true); });
|
|
300
|
+
document.addEventListener("focusout", function (e) { point(e, false); });
|
|
301
|
+
}
|
|
110
302
|
if (document.readyState === "loading") {
|
|
111
|
-
document.addEventListener("DOMContentLoaded", init);
|
|
303
|
+
document.addEventListener("DOMContentLoaded", function () { init(); listen(); });
|
|
112
304
|
} else {
|
|
113
305
|
init();
|
|
306
|
+
listen();
|
|
114
307
|
}
|
|
115
308
|
})();
|
|
116
309
|
`;
|