@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.
- package/README.md +34 -4
- package/common/ClientOnly.tsx +30 -0
- package/common/CopyButton.tsx +44 -0
- package/common/KindBadge.tsx +44 -0
- package/common/LoadingIndicator.tsx +104 -0
- package/common/PresetChips.tsx +43 -0
- package/common/cx.ts +34 -0
- package/common/useClipboard.ts +73 -0
- package/common/useDebouncedValue.ts +23 -0
- package/index.ts +83 -4
- package/out/common/ClientOnly.d.ts +22 -0
- package/out/common/ClientOnly.d.ts.map +1 -0
- package/out/common/ClientOnly.js +20 -0
- package/out/common/ClientOnly.js.map +1 -0
- package/out/common/CopyButton.d.ts +21 -0
- package/out/common/CopyButton.d.ts.map +1 -0
- package/out/common/CopyButton.js +7 -0
- package/out/common/CopyButton.js.map +1 -0
- package/out/common/KindBadge.d.ts +24 -0
- package/out/common/KindBadge.d.ts.map +1 -0
- package/out/common/KindBadge.js +6 -0
- package/out/common/KindBadge.js.map +1 -0
- package/out/common/LoadingIndicator.d.ts +27 -0
- package/out/common/LoadingIndicator.d.ts.map +1 -0
- package/out/common/LoadingIndicator.js +41 -0
- package/out/common/LoadingIndicator.js.map +1 -0
- package/out/common/PresetChips.d.ts +22 -0
- package/out/common/PresetChips.d.ts.map +1 -0
- package/out/common/PresetChips.js +5 -0
- package/out/common/PresetChips.js.map +1 -0
- package/out/common/cx.d.ts +13 -0
- package/out/common/cx.d.ts.map +1 -0
- package/out/common/cx.js +28 -0
- package/out/common/cx.js.map +1 -0
- package/out/common/useClipboard.d.ts +16 -0
- package/out/common/useClipboard.d.ts.map +1 -0
- package/out/common/useClipboard.js +56 -0
- package/out/common/useClipboard.js.map +1 -0
- package/out/common/useDebouncedValue.d.ts +11 -0
- package/out/common/useDebouncedValue.d.ts.map +1 -0
- package/out/common/useDebouncedValue.js +19 -0
- package/out/common/useDebouncedValue.js.map +1 -0
- package/out/index.d.ts +57 -4
- package/out/index.d.ts.map +1 -1
- package/out/index.js +37 -4
- package/out/index.js.map +1 -1
- package/out/pipeline/CandidatePicker.d.ts +17 -0
- package/out/pipeline/CandidatePicker.d.ts.map +1 -0
- package/out/pipeline/CandidatePicker.js +6 -0
- package/out/pipeline/CandidatePicker.js.map +1 -0
- package/out/pipeline/ComponentTable.d.ts +14 -0
- package/out/pipeline/ComponentTable.d.ts.map +1 -0
- package/out/pipeline/ComponentTable.js +6 -0
- package/out/pipeline/ComponentTable.js.map +1 -0
- package/out/pipeline/ConfidenceCell.d.ts +13 -0
- package/out/pipeline/ConfidenceCell.d.ts.map +1 -0
- package/out/pipeline/ConfidenceCell.js +22 -0
- package/out/pipeline/ConfidenceCell.js.map +1 -0
- package/out/pipeline/PipelineExplorer.d.ts +25 -0
- package/out/pipeline/PipelineExplorer.d.ts.map +1 -0
- package/out/pipeline/PipelineExplorer.js +27 -0
- package/out/pipeline/PipelineExplorer.js.map +1 -0
- package/out/pipeline/QueryForm.d.ts +19 -0
- package/out/pipeline/QueryForm.d.ts.map +1 -0
- package/out/pipeline/QueryForm.js +9 -0
- package/out/pipeline/QueryForm.js.map +1 -0
- package/out/pipeline/ResolvedPlace.d.ts +16 -0
- package/out/pipeline/ResolvedPlace.d.ts.map +1 -0
- package/out/pipeline/ResolvedPlace.js +14 -0
- package/out/pipeline/ResolvedPlace.js.map +1 -0
- package/out/pipeline/copy.d.ts +11 -0
- package/out/pipeline/copy.d.ts.map +1 -0
- package/out/pipeline/copy.js +31 -0
- package/out/pipeline/copy.js.map +1 -0
- package/out/pipeline/presets.d.ts +12 -0
- package/out/pipeline/presets.d.ts.map +1 -0
- package/out/pipeline/presets.js +21 -0
- package/out/pipeline/presets.js.map +1 -0
- package/out/pipeline/types.d.ts +110 -0
- package/out/pipeline/types.d.ts.map +1 -0
- package/out/pipeline/types.js +12 -0
- package/out/pipeline/types.js.map +1 -0
- package/out/pipeline/useParsePipeline.d.ts +34 -0
- package/out/pipeline/useParsePipeline.d.ts.map +1 -0
- package/out/pipeline/useParsePipeline.js +55 -0
- package/out/pipeline/useParsePipeline.js.map +1 -0
- package/out/poi/AbstainPanel.d.ts +14 -0
- package/out/poi/AbstainPanel.d.ts.map +1 -0
- package/out/poi/AbstainPanel.js +5 -0
- package/out/poi/AbstainPanel.js.map +1 -0
- package/out/poi/LiveResultsBlock.d.ts +21 -0
- package/out/poi/LiveResultsBlock.d.ts.map +1 -0
- package/out/poi/LiveResultsBlock.js +7 -0
- package/out/poi/LiveResultsBlock.js.map +1 -0
- package/out/poi/OverpassBlock.d.ts +15 -0
- package/out/poi/OverpassBlock.d.ts.map +1 -0
- package/out/poi/OverpassBlock.js +11 -0
- package/out/poi/OverpassBlock.js.map +1 -0
- package/out/poi/POIExplorer.d.ts +25 -0
- package/out/poi/POIExplorer.d.ts.map +1 -0
- package/out/poi/POIExplorer.js +31 -0
- package/out/poi/POIExplorer.js.map +1 -0
- package/out/poi/QueryInput.d.ts +17 -0
- package/out/poi/QueryInput.d.ts.map +1 -0
- package/out/poi/QueryInput.js +5 -0
- package/out/poi/QueryInput.js.map +1 -0
- package/out/poi/SubjectPanel.d.ts +15 -0
- package/out/poi/SubjectPanel.d.ts.map +1 -0
- package/out/poi/SubjectPanel.js +5 -0
- package/out/poi/SubjectPanel.js.map +1 -0
- package/out/poi/runtime.d.ts +34 -0
- package/out/poi/runtime.d.ts.map +1 -0
- package/out/poi/runtime.js +43 -0
- package/out/poi/runtime.js.map +1 -0
- package/out/poi/types.d.ts +86 -0
- package/out/poi/types.d.ts.map +1 -0
- package/out/poi/types.js +12 -0
- package/out/poi/types.js.map +1 -0
- package/out/poi/usePOISearch.d.ts +35 -0
- package/out/poi/usePOISearch.d.ts.map +1 -0
- package/out/poi/usePOISearch.js +123 -0
- package/out/poi/usePOISearch.js.map +1 -0
- package/package.json +52 -8
- package/pipeline/CandidatePicker.tsx +45 -0
- package/pipeline/ComponentTable.tsx +41 -0
- package/pipeline/ConfidenceCell.tsx +33 -0
- package/pipeline/PipelineExplorer.tsx +148 -0
- package/pipeline/QueryForm.tsx +52 -0
- package/pipeline/ResolvedPlace.tsx +50 -0
- package/pipeline/copy.ts +37 -0
- package/pipeline/presets.ts +24 -0
- package/pipeline/types.ts +118 -0
- package/pipeline/useParsePipeline.ts +84 -0
- package/poi/AbstainPanel.tsx +22 -0
- package/poi/LiveResultsBlock.tsx +70 -0
- package/poi/OverpassBlock.tsx +37 -0
- package/poi/POIExplorer.tsx +102 -0
- package/poi/QueryInput.tsx +26 -0
- package/poi/SubjectPanel.tsx +44 -0
- package/poi/runtime.ts +53 -0
- package/poi/types.ts +82 -0
- package/poi/usePOISearch.ts +183 -0
- 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
|
+
}
|
package/pipeline/copy.ts
ADDED
|
@@ -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
|
+
}
|