@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/site-rizom-ai",
3
- "version": "0.2.0-alpha.259",
3
+ "version": "0.2.0-alpha.261",
4
4
  "description": "Consolidated Rizom brand site, runtime, layouts, and chrome",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 key={`reply-${brain.id}`} className="net-reply">
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: { opening: openingTemplate, writing: writingTemplate },
23
- dataSourceFactories: [openingDataSource, writingDataSource],
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
  ],
@@ -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
  `;