@mailwoman/react 0.0.1 → 7.4.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.
Files changed (143) hide show
  1. package/README.md +34 -4
  2. package/common/ClientOnly.tsx +30 -0
  3. package/common/CopyButton.tsx +44 -0
  4. package/common/KindBadge.tsx +44 -0
  5. package/common/LoadingIndicator.tsx +104 -0
  6. package/common/PresetChips.tsx +43 -0
  7. package/common/cx.ts +34 -0
  8. package/common/useClipboard.ts +73 -0
  9. package/common/useDebouncedValue.ts +23 -0
  10. package/index.ts +83 -4
  11. package/out/common/ClientOnly.d.ts +22 -0
  12. package/out/common/ClientOnly.d.ts.map +1 -0
  13. package/out/common/ClientOnly.js +20 -0
  14. package/out/common/ClientOnly.js.map +1 -0
  15. package/out/common/CopyButton.d.ts +21 -0
  16. package/out/common/CopyButton.d.ts.map +1 -0
  17. package/out/common/CopyButton.js +7 -0
  18. package/out/common/CopyButton.js.map +1 -0
  19. package/out/common/KindBadge.d.ts +24 -0
  20. package/out/common/KindBadge.d.ts.map +1 -0
  21. package/out/common/KindBadge.js +6 -0
  22. package/out/common/KindBadge.js.map +1 -0
  23. package/out/common/LoadingIndicator.d.ts +27 -0
  24. package/out/common/LoadingIndicator.d.ts.map +1 -0
  25. package/out/common/LoadingIndicator.js +41 -0
  26. package/out/common/LoadingIndicator.js.map +1 -0
  27. package/out/common/PresetChips.d.ts +22 -0
  28. package/out/common/PresetChips.d.ts.map +1 -0
  29. package/out/common/PresetChips.js +5 -0
  30. package/out/common/PresetChips.js.map +1 -0
  31. package/out/common/cx.d.ts +13 -0
  32. package/out/common/cx.d.ts.map +1 -0
  33. package/out/common/cx.js +28 -0
  34. package/out/common/cx.js.map +1 -0
  35. package/out/common/useClipboard.d.ts +16 -0
  36. package/out/common/useClipboard.d.ts.map +1 -0
  37. package/out/common/useClipboard.js +56 -0
  38. package/out/common/useClipboard.js.map +1 -0
  39. package/out/common/useDebouncedValue.d.ts +11 -0
  40. package/out/common/useDebouncedValue.d.ts.map +1 -0
  41. package/out/common/useDebouncedValue.js +19 -0
  42. package/out/common/useDebouncedValue.js.map +1 -0
  43. package/out/index.d.ts +57 -4
  44. package/out/index.d.ts.map +1 -1
  45. package/out/index.js +37 -4
  46. package/out/index.js.map +1 -1
  47. package/out/pipeline/CandidatePicker.d.ts +17 -0
  48. package/out/pipeline/CandidatePicker.d.ts.map +1 -0
  49. package/out/pipeline/CandidatePicker.js +6 -0
  50. package/out/pipeline/CandidatePicker.js.map +1 -0
  51. package/out/pipeline/ComponentTable.d.ts +14 -0
  52. package/out/pipeline/ComponentTable.d.ts.map +1 -0
  53. package/out/pipeline/ComponentTable.js +6 -0
  54. package/out/pipeline/ComponentTable.js.map +1 -0
  55. package/out/pipeline/ConfidenceCell.d.ts +13 -0
  56. package/out/pipeline/ConfidenceCell.d.ts.map +1 -0
  57. package/out/pipeline/ConfidenceCell.js +22 -0
  58. package/out/pipeline/ConfidenceCell.js.map +1 -0
  59. package/out/pipeline/PipelineExplorer.d.ts +25 -0
  60. package/out/pipeline/PipelineExplorer.d.ts.map +1 -0
  61. package/out/pipeline/PipelineExplorer.js +27 -0
  62. package/out/pipeline/PipelineExplorer.js.map +1 -0
  63. package/out/pipeline/QueryForm.d.ts +19 -0
  64. package/out/pipeline/QueryForm.d.ts.map +1 -0
  65. package/out/pipeline/QueryForm.js +9 -0
  66. package/out/pipeline/QueryForm.js.map +1 -0
  67. package/out/pipeline/ResolvedPlace.d.ts +16 -0
  68. package/out/pipeline/ResolvedPlace.d.ts.map +1 -0
  69. package/out/pipeline/ResolvedPlace.js +14 -0
  70. package/out/pipeline/ResolvedPlace.js.map +1 -0
  71. package/out/pipeline/copy.d.ts +11 -0
  72. package/out/pipeline/copy.d.ts.map +1 -0
  73. package/out/pipeline/copy.js +31 -0
  74. package/out/pipeline/copy.js.map +1 -0
  75. package/out/pipeline/presets.d.ts +12 -0
  76. package/out/pipeline/presets.d.ts.map +1 -0
  77. package/out/pipeline/presets.js +21 -0
  78. package/out/pipeline/presets.js.map +1 -0
  79. package/out/pipeline/types.d.ts +110 -0
  80. package/out/pipeline/types.d.ts.map +1 -0
  81. package/out/pipeline/types.js +12 -0
  82. package/out/pipeline/types.js.map +1 -0
  83. package/out/pipeline/useParsePipeline.d.ts +34 -0
  84. package/out/pipeline/useParsePipeline.d.ts.map +1 -0
  85. package/out/pipeline/useParsePipeline.js +55 -0
  86. package/out/pipeline/useParsePipeline.js.map +1 -0
  87. package/out/poi/AbstainPanel.d.ts +14 -0
  88. package/out/poi/AbstainPanel.d.ts.map +1 -0
  89. package/out/poi/AbstainPanel.js +5 -0
  90. package/out/poi/AbstainPanel.js.map +1 -0
  91. package/out/poi/LiveResultsBlock.d.ts +21 -0
  92. package/out/poi/LiveResultsBlock.d.ts.map +1 -0
  93. package/out/poi/LiveResultsBlock.js +7 -0
  94. package/out/poi/LiveResultsBlock.js.map +1 -0
  95. package/out/poi/OverpassBlock.d.ts +15 -0
  96. package/out/poi/OverpassBlock.d.ts.map +1 -0
  97. package/out/poi/OverpassBlock.js +11 -0
  98. package/out/poi/OverpassBlock.js.map +1 -0
  99. package/out/poi/POIExplorer.d.ts +25 -0
  100. package/out/poi/POIExplorer.d.ts.map +1 -0
  101. package/out/poi/POIExplorer.js +31 -0
  102. package/out/poi/POIExplorer.js.map +1 -0
  103. package/out/poi/QueryInput.d.ts +17 -0
  104. package/out/poi/QueryInput.d.ts.map +1 -0
  105. package/out/poi/QueryInput.js +5 -0
  106. package/out/poi/QueryInput.js.map +1 -0
  107. package/out/poi/SubjectPanel.d.ts +15 -0
  108. package/out/poi/SubjectPanel.d.ts.map +1 -0
  109. package/out/poi/SubjectPanel.js +5 -0
  110. package/out/poi/SubjectPanel.js.map +1 -0
  111. package/out/poi/runtime.d.ts +34 -0
  112. package/out/poi/runtime.d.ts.map +1 -0
  113. package/out/poi/runtime.js +43 -0
  114. package/out/poi/runtime.js.map +1 -0
  115. package/out/poi/types.d.ts +86 -0
  116. package/out/poi/types.d.ts.map +1 -0
  117. package/out/poi/types.js +12 -0
  118. package/out/poi/types.js.map +1 -0
  119. package/out/poi/usePOISearch.d.ts +35 -0
  120. package/out/poi/usePOISearch.d.ts.map +1 -0
  121. package/out/poi/usePOISearch.js +123 -0
  122. package/out/poi/usePOISearch.js.map +1 -0
  123. package/package.json +52 -8
  124. package/pipeline/CandidatePicker.tsx +45 -0
  125. package/pipeline/ComponentTable.tsx +41 -0
  126. package/pipeline/ConfidenceCell.tsx +33 -0
  127. package/pipeline/PipelineExplorer.tsx +148 -0
  128. package/pipeline/QueryForm.tsx +52 -0
  129. package/pipeline/ResolvedPlace.tsx +50 -0
  130. package/pipeline/copy.ts +37 -0
  131. package/pipeline/presets.ts +24 -0
  132. package/pipeline/types.ts +118 -0
  133. package/pipeline/useParsePipeline.ts +84 -0
  134. package/poi/AbstainPanel.tsx +22 -0
  135. package/poi/LiveResultsBlock.tsx +70 -0
  136. package/poi/OverpassBlock.tsx +37 -0
  137. package/poi/POIExplorer.tsx +102 -0
  138. package/poi/QueryInput.tsx +26 -0
  139. package/poi/SubjectPanel.tsx +44 -0
  140. package/poi/runtime.ts +53 -0
  141. package/poi/types.ts +82 -0
  142. package/poi/usePOISearch.ts +183 -0
  143. package/styles.css +627 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ComponentTable` — the tag / value / confidence table of decoded components. Presentational.
7
+ */
8
+
9
+ import type { ReactNode } from "react"
10
+
11
+ import { ConfidenceCell } from "./ConfidenceCell.tsx"
12
+ import type { ParsedComponent } from "./types.ts"
13
+
14
+ export interface ComponentTableProps {
15
+ nodes: ParsedComponent[]
16
+ }
17
+
18
+ export function ComponentTable({ nodes }: ComponentTableProps): ReactNode {
19
+ return (
20
+ <table className="mw-components">
21
+ <thead>
22
+ <tr>
23
+ <th>tag</th>
24
+ <th>value</th>
25
+ <th>confidence</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ {nodes.map((node, i) => (
30
+ <tr key={i}>
31
+ <td>{node.tag}</td>
32
+ <td>{String(node.value ?? "")}</td>
33
+ <td>
34
+ <ConfidenceCell confidence={node.confidence} />
35
+ </td>
36
+ </tr>
37
+ ))}
38
+ </tbody>
39
+ </table>
40
+ )
41
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ConfidenceCell` — a compact confidence bar + value for the component table. Tiers the fill colour
7
+ * at 0.8 / 0.5. Presentational.
8
+ */
9
+
10
+ import { cx } from "../common/cx.ts"
11
+
12
+ function tier(confidence?: number): "high" | "mid" | "low" {
13
+ if (confidence == null) return "mid"
14
+
15
+ return confidence >= 0.8 ? "high" : confidence >= 0.5 ? "mid" : "low"
16
+ }
17
+
18
+ export interface ConfidenceCellProps {
19
+ confidence?: number
20
+ }
21
+
22
+ export function ConfidenceCell({ confidence }: ConfidenceCellProps) {
23
+ if (confidence == null) return <span className="mw-conf__dash">—</span>
24
+
25
+ const pct = Math.max(0, Math.min(1, confidence)) * 100
26
+
27
+ return (
28
+ <div className="mw-conf">
29
+ <div className={cx("mw-conf__bar", `mw-conf__bar--${tier(confidence)}`)} style={{ width: `${pct}%` }} />
30
+ <span className="mw-conf__value">{confidence.toFixed(2)}</span>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `PipelineExplorer` — the composed parse+resolve tester. Drives the injected {@link PipelineRuntime}
7
+ * through {@link useParsePipeline}, renders the core result (kind badge, component table, resolved
8
+ * place, candidate picker), and slots host-supplied panels (span highlight, tree, timing, …) around
9
+ * it. No model/gazetteer code here — composition + a `ClientOnly` SSR boundary only.
10
+ */
11
+
12
+ import type { ReactNode } from "react"
13
+
14
+ import { ClientOnly } from "../common/ClientOnly.tsx"
15
+ import { CopyButton } from "../common/CopyButton.tsx"
16
+ import { KindBadge } from "../common/KindBadge.tsx"
17
+ import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
18
+ import { PresetChips, type Preset } from "../common/PresetChips.tsx"
19
+ import { CandidatePicker } from "./CandidatePicker.tsx"
20
+ import { ComponentTable } from "./ComponentTable.tsx"
21
+ import { buildParsePayload } from "./copy.ts"
22
+ import { PIPELINE_DEFAULT_ADDRESS, PIPELINE_PRESETS } from "./presets.ts"
23
+ import { QueryForm } from "./QueryForm.tsx"
24
+ import { ResolvedPlace } from "./ResolvedPlace.tsx"
25
+ import type { PipelinePanels, PipelineRuntime } from "./types.ts"
26
+ import { useParsePipeline } from "./useParsePipeline.ts"
27
+
28
+ export interface PipelineExplorerProps {
29
+ /** The injected model/gazetteer runtime (the host wires this to its bundle). */
30
+ runtime: PipelineRuntime
31
+ /** Address to pre-fill. @default the White House */
32
+ defaultAddress?: string
33
+ /** Example chips. @default the built-in address presets */
34
+ presets?: ReadonlyArray<Preset>
35
+ /** Host-injected panels + controls (version selector, span highlight, tree, …). */
36
+ panels?: PipelinePanels
37
+ }
38
+
39
+ interface PipelineExplorerInnerProps extends Required<Pick<PipelineExplorerProps, "runtime" | "defaultAddress">> {
40
+ presets: ReadonlyArray<Preset>
41
+ panels: PipelinePanels
42
+ }
43
+
44
+ function PipelineExplorerInner({ runtime, defaultAddress, presets, panels }: PipelineExplorerInnerProps): ReactNode {
45
+ const pipeline = useParsePipeline({ runtime, defaultText: defaultAddress })
46
+ const { text, setText, busy, parseStage, result, parseError, selectedCandidate } = pipeline
47
+
48
+ const loading = runtime.loading
49
+ const errorMessage = parseError ?? runtime.errorMessage ?? null
50
+
51
+ return (
52
+ <div className="mw-pipeline-explorer">
53
+ {panels.header}
54
+ {panels.releaseInfo}
55
+ {panels.versionControl}
56
+ {panels.backendControl}
57
+
58
+ <QueryForm
59
+ value={text}
60
+ onChange={setText}
61
+ onSubmit={pipeline.submit}
62
+ disabled={!runtime.ready}
63
+ busy={busy}
64
+ placeholder={PIPELINE_DEFAULT_ADDRESS}
65
+ />
66
+
67
+ <PresetChips
68
+ presets={presets}
69
+ disabled={!runtime.ready || busy}
70
+ onPick={(value) => {
71
+ setText(value)
72
+ pipeline.reset()
73
+ }}
74
+ />
75
+
76
+ {loading && !runtime.ready ? (
77
+ <LoadingIndicator
78
+ mode="staged"
79
+ steps={loading.stepLabels.length > 0 ? loading.stepLabels : undefined}
80
+ activeStep={loading.stepIndex}
81
+ label={loading.progress}
82
+ />
83
+ ) : null}
84
+ {errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
85
+
86
+ {busy ? (
87
+ <div className="mw-result">
88
+ <LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={parseStage} />
89
+ </div>
90
+ ) : result ? (
91
+ <div className="mw-result">
92
+ <div className="mw-result__header">
93
+ <h2>Parsed components</h2>
94
+ <CopyButton
95
+ value={() => buildParsePayload(result, selectedCandidate)}
96
+ label="Copy JSON"
97
+ copiedLabel="✓ Copied"
98
+ />
99
+ </div>
100
+
101
+ {result.kindResult ? <KindBadge kindResult={result.kindResult} /> : null}
102
+
103
+ {panels.extras ? panels.extras(result) : null}
104
+
105
+ <ComponentTable nodes={result.nodes} />
106
+
107
+ {selectedCandidate ? (
108
+ <>
109
+ <ResolvedPlace place={selectedCandidate} dualRoles={result.dualRoles} />
110
+ {result.candidates.length > 1 ? (
111
+ <CandidatePicker
112
+ candidates={result.candidates}
113
+ selectedIndex={pipeline.selectedCandidateIndex}
114
+ onSelect={pipeline.selectCandidate}
115
+ />
116
+ ) : null}
117
+ </>
118
+ ) : panels.failure ? (
119
+ panels.failure(result)
120
+ ) : null}
121
+ </div>
122
+ ) : null}
123
+
124
+ {panels.footer}
125
+ </div>
126
+ )
127
+ }
128
+
129
+ export function PipelineExplorer({
130
+ runtime,
131
+ defaultAddress = PIPELINE_DEFAULT_ADDRESS,
132
+ presets = PIPELINE_PRESETS,
133
+ panels = {},
134
+ }: PipelineExplorerProps): ReactNode {
135
+ return (
136
+ <ClientOnly
137
+ fallback={
138
+ <div className="mw-pipeline-explorer">
139
+ <p>Loading demo embed…</p>
140
+ </div>
141
+ }
142
+ >
143
+ {() => (
144
+ <PipelineExplorerInner runtime={runtime} defaultAddress={defaultAddress} presets={presets} panels={panels} />
145
+ )}
146
+ </ClientOnly>
147
+ )
148
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `QueryForm` — the address input + submit button for the pipeline explorer. Shows a spinner while
7
+ * parsing. Presentational; `onSubmit` fires on form submission.
8
+ */
9
+
10
+ import type { ReactNode } from "react"
11
+
12
+ import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
13
+
14
+ export interface QueryFormProps {
15
+ value: string
16
+ onChange: (value: string) => void
17
+ onSubmit: () => void
18
+ disabled?: boolean
19
+ busy?: boolean
20
+ placeholder?: string
21
+ }
22
+
23
+ export function QueryForm({ value, onChange, onSubmit, disabled, busy, placeholder }: QueryFormProps): ReactNode {
24
+ return (
25
+ <form
26
+ className="mw-field"
27
+ onSubmit={(e) => {
28
+ e.preventDefault()
29
+ onSubmit()
30
+ }}
31
+ >
32
+ <label htmlFor="mw-pipeline-input">Address</label>
33
+ <input
34
+ id="mw-pipeline-input"
35
+ type="text"
36
+ value={value}
37
+ onChange={(e) => onChange(e.target.value)}
38
+ disabled={disabled}
39
+ placeholder={placeholder}
40
+ />
41
+ <button type="submit" disabled={disabled || busy}>
42
+ {busy ? (
43
+ <>
44
+ <LoadingIndicator mode="spinner" size="small" /> Parsing…
45
+ </>
46
+ ) : (
47
+ "Parse + resolve"
48
+ )}
49
+ </button>
50
+ </form>
51
+ )
52
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `ResolvedPlace` — the "Resolved place" detail list (name, placetype, WOF id, coords, score) plus an
7
+ * optional dual-role note. Presentational.
8
+ */
9
+
10
+ import { Fragment, type ReactNode } from "react"
11
+
12
+ import type { DualRoleView, ResolvedPlaceView } from "./types.ts"
13
+
14
+ export interface ResolvedPlaceProps {
15
+ place: ResolvedPlaceView
16
+ dualRoles?: DualRoleView[]
17
+ }
18
+
19
+ export function ResolvedPlace({ place, dualRoles }: ResolvedPlaceProps): ReactNode {
20
+ return (
21
+ <div className="mw-resolved">
22
+ <h2>Resolved place</h2>
23
+ <dl>
24
+ <dt>name</dt>
25
+ <dd>{place.name}</dd>
26
+ <dt>placetype</dt>
27
+ <dd>{place.placetype}</dd>
28
+ <dt>WOF id</dt>
29
+ <dd>{place.id}</dd>
30
+ <dt>coords</dt>
31
+ <dd>
32
+ {place.lat.toFixed(4)}, {place.lon.toFixed(4)}
33
+ </dd>
34
+ <dt>score</dt>
35
+ <dd>{place.score.toFixed(3)}</dd>
36
+ </dl>
37
+ {dualRoles && dualRoles.length > 0 ? (
38
+ <p className="mw-resolved__dual">
39
+ 🏛️ <strong>Dual-role place.</strong> {place.name} also resolves as{" "}
40
+ {dualRoles.map((role, i) => (
41
+ <Fragment key={`${role.role}-${role.id}`}>
42
+ {i > 0 ? ", " : ""}a <strong>{role.role}</strong> ({role.relationshipType.replace(/-/g, " ")})
43
+ </Fragment>
44
+ ))}
45
+ .
46
+ </p>
47
+ ) : null}
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Builds the "Copy JSON" payload for a parse result — the components + the selected resolved place,
7
+ * pretty-printed. Pure; shared by the explorer's copy button.
8
+ */
9
+
10
+ import type { ParseResult, ResolvedPlaceView } from "./types.ts"
11
+
12
+ export function buildParsePayload(result: ParseResult, selected: ResolvedPlaceView | null): string {
13
+ return JSON.stringify(
14
+ {
15
+ input: result.input,
16
+ components: result.nodes.map((node) => ({
17
+ tag: node.tag,
18
+ value: node.value ?? null,
19
+ confidence: node.confidence ?? null,
20
+ start: node.start ?? null,
21
+ end: node.end ?? null,
22
+ })),
23
+ resolved: selected
24
+ ? {
25
+ name: selected.name,
26
+ placetype: selected.placetype,
27
+ id: selected.id,
28
+ lat: selected.lat,
29
+ lon: selected.lon,
30
+ score: selected.score,
31
+ }
32
+ : null,
33
+ },
34
+ null,
35
+ 2
36
+ )
37
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Default query + example addresses for the pipeline explorer. Hosts can override via props; these
7
+ * mirror the docs demo's set so the extracted component looks identical out of the box.
8
+ */
9
+
10
+ import type { Preset } from "../common/PresetChips.tsx"
11
+
12
+ export const PIPELINE_DEFAULT_ADDRESS = "1600 Pennsylvania Ave NW, Washington, DC 20500"
13
+
14
+ export const PIPELINE_PRESETS: ReadonlyArray<Preset> = [
15
+ { label: "White House", value: "1600 Pennsylvania Ave NW, Washington, DC 20500" },
16
+ { label: "Apple Park", value: "1 Apple Park Way, Cupertino, CA 95014" },
17
+ { label: "30 Rockefeller Plaza", value: "30 Rockefeller Plaza, New York, NY 10112" },
18
+ { label: "Pier 39 SF", value: "Pier 39, San Francisco, CA 94133" },
19
+ { label: "Wrigley Field", value: "1060 W Addison St, Chicago, IL 60613" },
20
+ { label: "Space Needle", value: "400 Broad St, Seattle, WA 98109" },
21
+ { label: "ZIP only", value: "90210" },
22
+ { label: "Berlin (native order)", value: "Straußstraße 27, 12623 Berlin" },
23
+ { label: "Paris (street fall-through)", value: "181 Rue du Chevaleret, Paris" },
24
+ ]
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Types for the pipeline (parse + resolve) explorer. The model/gazetteer runtime is INJECTED as a
7
+ * {@link PipelineRuntime}: this package owns the UI state machine + presentation, while the host
8
+ * (the docs site's DemoEmbed, or any app) owns loading ONNX/WOF and executing a parse. That keeps
9
+ * onnxruntime-web, sql.js-httpvfs, and node builtins entirely out of this package's browser graph.
10
+ */
11
+
12
+ import type { ReactNode } from "react"
13
+
14
+ import type { KindBadgeResult } from "../common/KindBadge.tsx"
15
+
16
+ /** One decoded component, as the table + span views render it. Offsets index into `ParseResult.input`. */
17
+ export interface ParsedComponent {
18
+ tag: string
19
+ value?: unknown
20
+ confidence?: number
21
+ start?: number
22
+ end?: number
23
+ }
24
+
25
+ /** A resolved gazetteer place, projected to what the "Resolved place" panel + copy payload need. */
26
+ export interface ResolvedPlaceView {
27
+ id: number
28
+ name: string
29
+ placetype: string
30
+ lat: number
31
+ lon: number
32
+ score: number
33
+ }
34
+
35
+ /** An additional admin role a resolved place also fulfils (the dual-role / city-state relation, #402). */
36
+ export interface DualRoleView {
37
+ id: number
38
+ name: string
39
+ placetype: string
40
+ relationshipType: string
41
+ role: string
42
+ }
43
+
44
+ /** Per-stage wall-clock (ms) for one parse. `resolve` is absent when the lookup is skipped. */
45
+ export interface StageTiming {
46
+ shape: number
47
+ classify: number
48
+ resolve?: number
49
+ }
50
+
51
+ /** FST gazetteer-prior provenance, as the "FST prior" disclosure renders it. */
52
+ export interface FSTProvenance {
53
+ builtAt: string
54
+ stateCount: number
55
+ placeCount: number
56
+ importanceMatches: number
57
+ }
58
+
59
+ /** The presentational projection of one parse+resolve. The host's `runParse` produces it. */
60
+ export interface ParseResult {
61
+ input: string
62
+ /** Opaque hierarchy — handed straight to a host-injected tree/visualizer panel. */
63
+ tree: unknown
64
+ nodes: ParsedComponent[]
65
+ kindResult?: KindBadgeResult
66
+ timing?: StageTiming
67
+ resolved: ResolvedPlaceView | null
68
+ candidates: ResolvedPlaceView[]
69
+ fstActive: boolean
70
+ fstProvenance?: FSTProvenance | null
71
+ dualRoles?: DualRoleView[]
72
+ }
73
+
74
+ /** Bundle-load progress surfaced before the runtime is `ready`. */
75
+ export interface PipelineLoadingState {
76
+ progress?: string
77
+ stepLabels: string[]
78
+ stepIndex: number
79
+ }
80
+
81
+ /**
82
+ * The injected parse runtime. The host implements `runParse` (compute shape → classify → resolve) and reports load
83
+ * progress + errors. This package never imports the model or gazetteer — it only calls this contract.
84
+ */
85
+ export interface PipelineRuntime {
86
+ /** Whether the model/gazetteer bundle is ready to parse. */
87
+ ready: boolean
88
+ /** Execute a full parse+resolve, reporting stage progress via `onStage` (0-based index into `parseStageLabels`). */
89
+ runParse: (input: string, hooks: { onStage: (stage: number) => void }) => Promise<ParseResult>
90
+ /** Per-parse progress labels (differ by whether a gazetteer lookup is wired). */
91
+ parseStageLabels: string[]
92
+ /** Bundle-load progress (before `ready`). */
93
+ loading?: PipelineLoadingState | null
94
+ /** Error surfaced by the bundle load, distinct from a per-parse error. */
95
+ errorMessage?: string | null
96
+ }
97
+
98
+ /**
99
+ * Optional host-injected panels. Each is a function of the current parse result returning already-rendered content (the
100
+ * docs site's SpanHighlight, TreeView, TimingPanel, …). Kept as `ReactNode` thunks so this package needs neither those
101
+ * components nor their heavy data types.
102
+ */
103
+ export interface PipelinePanels {
104
+ /** Rendered above the form (e.g. the docs "About this demo"). */
105
+ header?: ReactNode
106
+ /** Rendered below everything (e.g. a guided tour). */
107
+ footer?: ReactNode
108
+ /** Model-version selector, wired to the host's bundle state. */
109
+ versionControl?: ReactNode
110
+ /** Backend indicator + WASM toggle. */
111
+ backendControl?: ReactNode
112
+ /** One-line release blurb. */
113
+ releaseInfo?: ReactNode
114
+ /** Heavy visualizers (span highlight, tree, timing, BIO, …), rendered from the result. */
115
+ extras?: (result: ParseResult) => ReactNode
116
+ /** Rendered in place of the resolved-place panel when nothing resolved (host's FailureDiagnostic). */
117
+ failure?: (result: ParseResult) => ReactNode
118
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useParsePipeline` — the headless core of the pipeline explorer. Owns the query text, the
7
+ * busy/stage flags, the parse result, and the candidate selection; delegates the actual parse+resolve
8
+ * to the injected {@link PipelineRuntime}. No model or gazetteer code lives here — it just drives the
9
+ * contract and shapes the UI state.
10
+ */
11
+
12
+ import { useCallback, useState } from "react"
13
+
14
+ import type { ParseResult, PipelineRuntime, ResolvedPlaceView } from "./types.ts"
15
+
16
+ export interface UseParsePipelineOptions {
17
+ runtime: PipelineRuntime
18
+ defaultText: string
19
+ }
20
+
21
+ export interface UseParsePipeline {
22
+ text: string
23
+ setText: (text: string) => void
24
+ busy: boolean
25
+ /** 0-based index into `runtime.parseStageLabels`; -1 when idle. */
26
+ parseStage: number
27
+ result: ParseResult | null
28
+ parseError: string | null
29
+ selectedCandidateIndex: number
30
+ selectCandidate: (index: number) => void
31
+ /** The currently-selected candidate (falls back to the first), or null. */
32
+ selectedCandidate: ResolvedPlaceView | null
33
+ /** Run a parse for the current text. Safe to bind to a form's `onSubmit`. */
34
+ submit: () => Promise<void>
35
+ /** Clear the result (used when a preset replaces the input). */
36
+ reset: () => void
37
+ }
38
+
39
+ export function useParsePipeline({ runtime, defaultText }: UseParsePipelineOptions): UseParsePipeline {
40
+ const [text, setText] = useState(defaultText)
41
+ const [busy, setBusy] = useState(false)
42
+ const [parseStage, setParseStage] = useState(-1)
43
+ const [result, setResult] = useState<ParseResult | null>(null)
44
+ const [selectedCandidateIndex, setSelectedCandidateIndex] = useState(0)
45
+ const [parseError, setParseError] = useState<string | null>(null)
46
+
47
+ const submit = useCallback(async () => {
48
+ if (!runtime.ready || busy) return
49
+
50
+ setBusy(true)
51
+ setParseStage(0)
52
+ setParseError(null)
53
+
54
+ try {
55
+ const parsed = await runtime.runParse(text, { onStage: setParseStage })
56
+ setSelectedCandidateIndex(0)
57
+ setResult(parsed)
58
+ } catch (err) {
59
+ setParseError(err instanceof Error ? err.message : String(err))
60
+ } finally {
61
+ setBusy(false)
62
+ setParseStage(-1)
63
+ }
64
+ }, [runtime, text, busy])
65
+
66
+ const reset = useCallback(() => setResult(null), [])
67
+
68
+ // Derived during render — no effect needed.
69
+ const selectedCandidate = result ? (result.candidates[selectedCandidateIndex] ?? result.candidates[0] ?? null) : null
70
+
71
+ return {
72
+ text,
73
+ setText,
74
+ busy,
75
+ parseStage,
76
+ result,
77
+ parseError,
78
+ selectedCandidateIndex,
79
+ selectCandidate: setSelectedCandidateIndex,
80
+ selectedCandidate,
81
+ submit,
82
+ reset,
83
+ }
84
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `AbstainPanel` — shown when the query carries no POI intent (it parses as an address). Names the
7
+ * classifier's kind so the abstention is legible. Presentational.
8
+ */
9
+
10
+ import type { ReactNode } from "react"
11
+
12
+ export interface AbstainPanelProps {
13
+ kind: string
14
+ }
15
+
16
+ export function AbstainPanel({ kind }: AbstainPanelProps): ReactNode {
17
+ return (
18
+ <p className="mw-muted">
19
+ No POI intent detected — parses as an address (kind <code>{kind}</code>).
20
+ </p>
21
+ )
22
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `LiveResultsBlock` — the on-demand live poi.db search affordance: a "Search live" button plus the
7
+ * idle / loading / error / results states. Presentational — all behavior comes in via props (the
8
+ * `usePOISearch` hook owns the state machine + the injected probe).
9
+ */
10
+
11
+ import type { ReactNode } from "react"
12
+
13
+ import { formatDistance } from "./runtime.ts"
14
+ import type { LiveSearchState } from "./types.ts"
15
+
16
+ export interface LiveResultsBlockProps {
17
+ /** Category label for empty-state copy ("No hospital results near …"). */
18
+ categoryLabel: string
19
+ /** The location anchor the search runs against (empty ⇒ the button is disabled with a hint). */
20
+ anchor: string
21
+ state: LiveSearchState
22
+ onSearch: () => void
23
+ }
24
+
25
+ export function LiveResultsBlock({ categoryLabel, anchor, state, onSearch }: LiveResultsBlockProps): ReactNode {
26
+ const hasAnchor = anchor.trim().length > 0
27
+
28
+ return (
29
+ <div className="mw-live">
30
+ <div className="mw-panel__header">
31
+ <h3>Live results</h3>
32
+ <button
33
+ type="button"
34
+ className="mw-btn"
35
+ onClick={onSearch}
36
+ disabled={!hasAnchor || state.status === "loading"}
37
+ title={hasAnchor ? "Search the published poi.db layer" : 'Needs a location anchor (e.g. "near Springfield")'}
38
+ >
39
+ {state.status === "loading" ? "Searching…" : "Search live"}
40
+ </button>
41
+ </div>
42
+
43
+ {!hasAnchor ? (
44
+ <p className="mw-muted">Add a location anchor (e.g. "near Springfield") to search live.</p>
45
+ ) : state.status === "error" ? (
46
+ <p className="mw-error">{state.message}</p>
47
+ ) : state.status === "success" ? (
48
+ state.hits.length === 0 ? (
49
+ <p className="mw-muted">
50
+ No {categoryLabel.toLowerCase()} results near {state.centerName}.
51
+ </p>
52
+ ) : (
53
+ <>
54
+ <p className="mw-live__caption">Near {state.centerName}, ranked by distance:</p>
55
+ <ul className="mw-live__results">
56
+ {state.hits.map((hit, i) => (
57
+ <li key={`${hit.name}-${i}`}>
58
+ <span className="mw-live__name">{hit.name}</span>
59
+ <span className="mw-live__meta">
60
+ {formatDistance(hit.distanceM)} · {hit.country}
61
+ </span>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </>
66
+ )
67
+ ) : null}
68
+ </div>
69
+ )
70
+ }