@shortlink-org/portolan 0.1.0 → 0.2.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 +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +22 -4
- package/scripts/local-api.test.mjs +84 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +6 -2
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +12 -8
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useMemo, useRef } from "react";
|
|
9
9
|
import type { MouseEvent as ReactMouseEvent } from "react";
|
|
10
|
-
import { useNavigate } from "react-router";
|
|
10
|
+
import { useLocation, useNavigate } from "react-router";
|
|
11
11
|
import { Chat, useChat } from "@ai-sdk/react";
|
|
12
12
|
import { getToolName, isToolUIPart } from "ai";
|
|
13
13
|
import type { ChatStatus, UIMessage } from "ai";
|
|
14
14
|
import { Markdown } from "../components/Markdown";
|
|
15
|
-
import { catalog } from "../data";
|
|
15
|
+
import { activeCatalogProfile, catalog } from "../data";
|
|
16
16
|
import { paletteItems } from "../lib/palette";
|
|
17
17
|
import { ToolCard, toolOutput } from "./Cards";
|
|
18
18
|
import { Composer } from "./Composer";
|
|
@@ -30,10 +30,12 @@ import type { Read } from "./Steps";
|
|
|
30
30
|
import { useChatUi } from "./store";
|
|
31
31
|
import { transportFor } from "./transport";
|
|
32
32
|
import { Sprite } from "./Waiting";
|
|
33
|
+
import { pageContext } from "./page-context";
|
|
33
34
|
|
|
34
35
|
export type Answering = Extract<ChatRoute, { kind: "proxy" | "own" }>;
|
|
35
36
|
|
|
36
37
|
const base = import.meta.env.BASE_URL;
|
|
38
|
+
const stayHere = () => {};
|
|
37
39
|
|
|
38
40
|
/** Every id the catalog has a page for, and the page. */
|
|
39
41
|
const LINKS: ReadonlyMap<string, string> = (() => {
|
|
@@ -91,7 +93,8 @@ function waitingLabel(
|
|
|
91
93
|
if (!last || last.role !== "assistant") return "thinking…";
|
|
92
94
|
const part = last.parts[last.parts.length - 1];
|
|
93
95
|
if (!part) return "thinking…";
|
|
94
|
-
if (part.type === "text")
|
|
96
|
+
if (part.type === "text")
|
|
97
|
+
return part.state === "streaming" ? null : "thinking…";
|
|
95
98
|
if (isToolUIPart(part) && getToolName(part) === "read_page") {
|
|
96
99
|
if (part.state === "input-streaming" || part.state === "input-available") {
|
|
97
100
|
return `reading ${pathOf(part.input) ?? "a page"}`;
|
|
@@ -138,7 +141,12 @@ function Answer({
|
|
|
138
141
|
}
|
|
139
142
|
if (isDisplayTool(name) && part.state !== "input-streaming") {
|
|
140
143
|
return (
|
|
141
|
-
<ToolCard
|
|
144
|
+
<ToolCard
|
|
145
|
+
key={index}
|
|
146
|
+
name={name}
|
|
147
|
+
input={part.input}
|
|
148
|
+
onNavigate={onNavigate}
|
|
149
|
+
/>
|
|
142
150
|
);
|
|
143
151
|
}
|
|
144
152
|
return null;
|
|
@@ -147,16 +155,41 @@ function Answer({
|
|
|
147
155
|
);
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
function SeededExchange({ onNavigate }: { onNavigate: () => void }) {
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
<Message role="user">Show the “Checkout” flow.</Message>
|
|
162
|
+
<Message role="assistant" foot="grounded in flows/cart-checkout.md">
|
|
163
|
+
<ReadSteps reads={[{ path: "flows/cart-checkout.md" }]} />
|
|
164
|
+
<p className="leading-6 text-muted">
|
|
165
|
+
<span className="font-medium text-ink">shop.cart</span> validates the
|
|
166
|
+
session with auth.auth, loads the basket, requests its total from
|
|
167
|
+
shop.pricing, saves it, then publishes BasketCheckedOut.
|
|
168
|
+
</p>
|
|
169
|
+
<ToolCard
|
|
170
|
+
name="show_flow"
|
|
171
|
+
input={{ id: "flow.cart-checkout" }}
|
|
172
|
+
onNavigate={onNavigate}
|
|
173
|
+
/>
|
|
174
|
+
</Message>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
export function Conversation({
|
|
151
180
|
route,
|
|
152
181
|
onOwnKey,
|
|
153
182
|
onSettings,
|
|
154
183
|
onClose,
|
|
184
|
+
seeded = false,
|
|
155
185
|
}: {
|
|
156
186
|
route: Answering;
|
|
157
187
|
onOwnKey: () => void;
|
|
158
188
|
onSettings: () => void;
|
|
159
|
-
|
|
189
|
+
/** Omitted when the conversation is embedded in a page. */
|
|
190
|
+
onClose?: () => void;
|
|
191
|
+
/** Show one useful, catalog-grounded exchange before the reader asks. */
|
|
192
|
+
seeded?: boolean;
|
|
160
193
|
}) {
|
|
161
194
|
const chat = useMemo(() => chatFor(route), [route]);
|
|
162
195
|
const {
|
|
@@ -173,6 +206,8 @@ export function Conversation({
|
|
|
173
206
|
const phase = useChatUi((s) => s.phase);
|
|
174
207
|
const took = useChatUi((s) => s.took);
|
|
175
208
|
const navigate = useNavigate();
|
|
209
|
+
const location = useLocation();
|
|
210
|
+
const page = useMemo(() => pageContext(location.pathname), [location.pathname]);
|
|
176
211
|
const list = useRef<HTMLDivElement>(null);
|
|
177
212
|
const busy = status === "submitted" || status === "streaming";
|
|
178
213
|
const waiting = waitingLabel(messages, status, phase);
|
|
@@ -205,7 +240,15 @@ export function Conversation({
|
|
|
205
240
|
const send = (question: string) => {
|
|
206
241
|
if (busy) return;
|
|
207
242
|
useChatUi.getState().setAskedAt(Date.now());
|
|
208
|
-
void sendMessage(
|
|
243
|
+
void sendMessage(
|
|
244
|
+
{ text: question },
|
|
245
|
+
{
|
|
246
|
+
body: {
|
|
247
|
+
catalogId: activeCatalogProfile.id,
|
|
248
|
+
...(page ? { pageContext: page } : {}),
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
);
|
|
209
252
|
};
|
|
210
253
|
|
|
211
254
|
// Links in an answer point into the app; follow them without a reload, and
|
|
@@ -218,16 +261,19 @@ export function Conversation({
|
|
|
218
261
|
if (!href.startsWith(base) || /^[a-z]+:/i.test(href)) return;
|
|
219
262
|
event.preventDefault();
|
|
220
263
|
navigate(`/${href.slice(base.length)}`);
|
|
221
|
-
onClose();
|
|
264
|
+
onClose?.();
|
|
222
265
|
};
|
|
223
266
|
|
|
224
267
|
return (
|
|
225
268
|
<>
|
|
226
269
|
<Header
|
|
227
270
|
route={route}
|
|
271
|
+
page={page}
|
|
228
272
|
onSettings={onSettings}
|
|
229
|
-
onClose
|
|
230
|
-
{...(messages.length > 0 && !busy
|
|
273
|
+
{...(onClose ? { onClose } : {})}
|
|
274
|
+
{...(messages.length > 0 && !busy
|
|
275
|
+
? { onClear: () => setMessages([]) }
|
|
276
|
+
: {})}
|
|
231
277
|
/>
|
|
232
278
|
<div
|
|
233
279
|
ref={list}
|
|
@@ -236,19 +282,27 @@ export function Conversation({
|
|
|
236
282
|
aria-live="polite"
|
|
237
283
|
className="min-h-0 flex-1 space-y-5 overflow-y-auto px-4 py-4"
|
|
238
284
|
>
|
|
239
|
-
{
|
|
285
|
+
{seeded ? (
|
|
286
|
+
<SeededExchange onNavigate={onClose ?? stayHere} />
|
|
287
|
+
) : messages.length === 0 ? (
|
|
288
|
+
<Starter onAsk={send} page={page} />
|
|
289
|
+
) : null}
|
|
240
290
|
{messages.map((message) =>
|
|
241
291
|
message.role === "user" ? (
|
|
242
292
|
<Message key={message.id} role="user">
|
|
243
|
-
{message.parts
|
|
293
|
+
{message.parts
|
|
294
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
295
|
+
.join("")}
|
|
244
296
|
</Message>
|
|
245
297
|
) : message.role === "assistant" ? (
|
|
246
298
|
<Message
|
|
247
299
|
key={message.id}
|
|
248
300
|
role="assistant"
|
|
249
|
-
{...(took[message.id] !== undefined
|
|
301
|
+
{...(took[message.id] !== undefined
|
|
302
|
+
? { foot: `took ${took[message.id]}s` }
|
|
303
|
+
: {})}
|
|
250
304
|
>
|
|
251
|
-
<Answer message={message} onNavigate={onClose} />
|
|
305
|
+
<Answer message={message} onNavigate={onClose ?? stayHere} />
|
|
252
306
|
</Message>
|
|
253
307
|
) : null,
|
|
254
308
|
)}
|
|
@@ -268,14 +322,24 @@ export function Conversation({
|
|
|
268
322
|
onRetry={() => {
|
|
269
323
|
clearError();
|
|
270
324
|
useChatUi.getState().setAskedAt(Date.now());
|
|
271
|
-
void regenerate(
|
|
325
|
+
void regenerate({
|
|
326
|
+
body: {
|
|
327
|
+
catalogId: activeCatalogProfile.id,
|
|
328
|
+
...(page ? { pageContext: page } : {}),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
272
331
|
}}
|
|
273
332
|
onOwnKey={onOwnKey}
|
|
274
333
|
/>
|
|
275
334
|
</div>
|
|
276
335
|
) : null}
|
|
277
336
|
</div>
|
|
278
|
-
<Composer
|
|
337
|
+
<Composer
|
|
338
|
+
busy={busy}
|
|
339
|
+
onSend={send}
|
|
340
|
+
onStop={() => void stop()}
|
|
341
|
+
focusOnMount={!seeded}
|
|
342
|
+
/>
|
|
279
343
|
</>
|
|
280
344
|
);
|
|
281
345
|
}
|
package/src/chat/Header.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Eraser, MessageSquare, Settings2, X } from "lucide-react";
|
|
4
4
|
import { routeLabel } from "./flags";
|
|
5
5
|
import type { ChatRoute } from "./flags";
|
|
6
|
+
import type { ChatPageContext } from "./page-context";
|
|
6
7
|
|
|
7
8
|
const ICON =
|
|
8
9
|
"flex size-8 items-center justify-center rounded-control text-muted t-micro transition-colors hover:bg-surface hover:text-ink";
|
|
@@ -12,34 +13,67 @@ export function Header({
|
|
|
12
13
|
onSettings,
|
|
13
14
|
onClear,
|
|
14
15
|
onClose,
|
|
16
|
+
page,
|
|
15
17
|
}: {
|
|
16
18
|
route: ChatRoute;
|
|
19
|
+
page?: ChatPageContext | null;
|
|
17
20
|
onSettings: () => void;
|
|
18
21
|
/** Present once there is a conversation to start over from. */
|
|
19
22
|
onClear?: () => void;
|
|
20
|
-
|
|
23
|
+
/** Omitted when the conversation is embedded in a page. */
|
|
24
|
+
onClose?: () => void;
|
|
21
25
|
}) {
|
|
22
26
|
return (
|
|
23
27
|
<div className="flex items-center gap-2.5 border-b border-line px-4 py-2.5">
|
|
24
28
|
<MessageSquare size={16} aria-hidden className="shrink-0 text-accent" />
|
|
25
29
|
<div className="min-w-0">
|
|
26
|
-
<div className="font-semibold leading-tight text-ink">
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
<div className="font-semibold leading-tight text-ink">
|
|
31
|
+
ask the catalog
|
|
32
|
+
</div>
|
|
33
|
+
<div className="mono flex min-w-0 items-center gap-1.5 truncate text-muted">
|
|
34
|
+
<span className="truncate">{routeLabel(route) || "nothing set to answer"}</span>
|
|
35
|
+
{page ? (
|
|
36
|
+
<>
|
|
37
|
+
<span aria-hidden className="text-faint">·</span>
|
|
38
|
+
<span className="truncate text-ink" title={`${page.kind} ${page.id}`}>
|
|
39
|
+
{page.kind} · {page.title}
|
|
40
|
+
</span>
|
|
41
|
+
</>
|
|
42
|
+
) : null}
|
|
29
43
|
</div>
|
|
30
44
|
</div>
|
|
31
45
|
<div className="ml-auto flex shrink-0 items-center">
|
|
32
46
|
{onClear ? (
|
|
33
|
-
<button
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={onClear}
|
|
50
|
+
aria-label="New conversation"
|
|
51
|
+
title="New conversation"
|
|
52
|
+
className={ICON}
|
|
53
|
+
>
|
|
34
54
|
<Eraser size={15} aria-hidden />
|
|
35
55
|
</button>
|
|
36
56
|
) : null}
|
|
37
|
-
<button
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={onSettings}
|
|
60
|
+
aria-label="Model settings"
|
|
61
|
+
title="Model settings"
|
|
62
|
+
className={ICON}
|
|
63
|
+
>
|
|
38
64
|
<Settings2 size={15} aria-hidden />
|
|
39
65
|
</button>
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
66
|
+
{onClose ? (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={onClose}
|
|
70
|
+
aria-label="Close"
|
|
71
|
+
title="Close — Esc"
|
|
72
|
+
className={ICON}
|
|
73
|
+
>
|
|
74
|
+
<X size={15} aria-hidden />
|
|
75
|
+
</button>
|
|
76
|
+
) : null}
|
|
43
77
|
</div>
|
|
44
78
|
</div>
|
|
45
79
|
);
|
package/src/chat/Starter.tsx
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { KindIcon } from "../components/kind";
|
|
8
8
|
import type { Kind } from "../lib/kinds";
|
|
9
|
-
import { catalog } from "../data";
|
|
9
|
+
import { activeCatalogProfile, catalog } from "../data";
|
|
10
|
+
import type { ChatPageContext } from "./page-context";
|
|
11
|
+
import { contextQuestions } from "./page-context";
|
|
10
12
|
|
|
11
13
|
interface Example {
|
|
12
14
|
kind: Kind;
|
|
@@ -34,7 +36,26 @@ function examples(): Example[] {
|
|
|
34
36
|
return out;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const base = import.meta.env.BASE_URL;
|
|
40
|
+
|
|
41
|
+
export function Starter({
|
|
42
|
+
onAsk,
|
|
43
|
+
page,
|
|
44
|
+
}: {
|
|
45
|
+
onAsk: (question: string) => void;
|
|
46
|
+
page?: ChatPageContext | null;
|
|
47
|
+
}) {
|
|
48
|
+
const choices: Example[] = page
|
|
49
|
+
? contextQuestions(page).map((question) => ({
|
|
50
|
+
kind: page.kind,
|
|
51
|
+
question,
|
|
52
|
+
}))
|
|
53
|
+
: examples();
|
|
54
|
+
const profileSuffix =
|
|
55
|
+
activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
|
|
56
|
+
const docsRoot = `${base}docs/${profileSuffix}`;
|
|
57
|
+
const indexHref = profileSuffix ? `${docsRoot}llms.txt` : `${base}llms.txt`;
|
|
58
|
+
|
|
38
59
|
return (
|
|
39
60
|
<div className="pt-1">
|
|
40
61
|
<p className="text-muted">
|
|
@@ -42,9 +63,49 @@ export function Starter({ onAsk }: { onAsk: (question: string) => void }) {
|
|
|
42
63
|
link. Ask about a service, a flow, a decision, or what stands between two
|
|
43
64
|
contexts.
|
|
44
65
|
</p>
|
|
66
|
+
{page ? (
|
|
67
|
+
<div className="mt-4 rounded-card border border-line bg-surface px-3 py-2.5 shadow-xs">
|
|
68
|
+
<div className="flex min-w-0 items-start gap-2.5">
|
|
69
|
+
<KindIcon
|
|
70
|
+
kind={page.kind}
|
|
71
|
+
size={16}
|
|
72
|
+
className="mt-0.5 shrink-0"
|
|
73
|
+
/>
|
|
74
|
+
<div className="min-w-0 flex-1">
|
|
75
|
+
<div className="label">current page</div>
|
|
76
|
+
<div className="mt-0.5 truncate font-medium text-ink" title={page.title}>
|
|
77
|
+
{page.title}
|
|
78
|
+
</div>
|
|
79
|
+
<div className="mono truncate text-muted" title={page.id}>{page.id}</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="mono mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-line pt-2 text-muted">
|
|
83
|
+
<span className="text-faint">model context</span>
|
|
84
|
+
<a
|
|
85
|
+
href={indexHref}
|
|
86
|
+
target="_blank"
|
|
87
|
+
rel="noreferrer"
|
|
88
|
+
className="rounded-control text-accent hover:underline"
|
|
89
|
+
>
|
|
90
|
+
llms.txt
|
|
91
|
+
</a>
|
|
92
|
+
{page.docPath ? (
|
|
93
|
+
<a
|
|
94
|
+
href={`${docsRoot}${page.docPath}`}
|
|
95
|
+
target="_blank"
|
|
96
|
+
rel="noreferrer"
|
|
97
|
+
className="max-w-full truncate rounded-control text-accent hover:underline"
|
|
98
|
+
title={page.docPath}
|
|
99
|
+
>
|
|
100
|
+
current catalog page
|
|
101
|
+
</a>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
45
106
|
<div className="label mt-5 mb-1">try one</div>
|
|
46
107
|
<ul className="-mx-1">
|
|
47
|
-
{
|
|
108
|
+
{choices.map((example) => (
|
|
48
109
|
<li key={example.question}>
|
|
49
110
|
<button
|
|
50
111
|
type="button"
|
package/src/chat/Steps.tsx
CHANGED
|
@@ -24,7 +24,7 @@ export function ReadSteps({ reads }: { reads: Read[] }) {
|
|
|
24
24
|
className="transition-transform group-open:rotate-90"
|
|
25
25
|
/>
|
|
26
26
|
<FileText size={13} aria-hidden />
|
|
27
|
-
|
|
27
|
+
sources · {reads.length} {reads.length === 1 ? "page" : "pages"}
|
|
28
28
|
</summary>
|
|
29
29
|
<ul className="mono mt-1 space-y-0.5 pl-6">
|
|
30
30
|
{reads.map((read, index) => (
|
package/src/chat/flags.test.ts
CHANGED
|
@@ -2,18 +2,32 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { chatRoute, ownReady, parsePrefs, routeLabel } from "./flags";
|
|
3
3
|
import type { ChatBuild, OwnModel } from "./flags";
|
|
4
4
|
|
|
5
|
-
const withProxy: ChatBuild = {
|
|
5
|
+
const withProxy: ChatBuild = {
|
|
6
|
+
built: true,
|
|
7
|
+
proxyUrl: "https://x.workers.dev/chat",
|
|
8
|
+
};
|
|
6
9
|
const noProxy: ChatBuild = { built: true, proxyUrl: "" };
|
|
7
|
-
const notBuilt: ChatBuild = {
|
|
8
|
-
|
|
10
|
+
const notBuilt: ChatBuild = {
|
|
11
|
+
built: false,
|
|
12
|
+
proxyUrl: "https://x.workers.dev/chat",
|
|
13
|
+
};
|
|
14
|
+
const own: OwnModel = {
|
|
15
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
16
|
+
apiKey: "k",
|
|
17
|
+
model: "llama",
|
|
18
|
+
};
|
|
9
19
|
|
|
10
20
|
describe("chatRoute", () => {
|
|
11
21
|
it("is off when the build has no chat, whatever the reader set", () => {
|
|
12
|
-
expect(chatRoute(notBuilt, { enabled: true, own })).toEqual({
|
|
22
|
+
expect(chatRoute(notBuilt, { enabled: true, own })).toEqual({
|
|
23
|
+
kind: "off",
|
|
24
|
+
});
|
|
13
25
|
});
|
|
14
26
|
|
|
15
27
|
it("is off when the reader switched it off, even with a proxy and a key", () => {
|
|
16
|
-
expect(chatRoute(withProxy, { enabled: false, own })).toEqual({
|
|
28
|
+
expect(chatRoute(withProxy, { enabled: false, own })).toEqual({
|
|
29
|
+
kind: "off",
|
|
30
|
+
});
|
|
17
31
|
});
|
|
18
32
|
|
|
19
33
|
it("uses the proxy by default when the build has one", () => {
|
|
@@ -24,25 +38,43 @@ describe("chatRoute", () => {
|
|
|
24
38
|
});
|
|
25
39
|
|
|
26
40
|
it("prefers the reader's own model over the proxy", () => {
|
|
27
|
-
expect(chatRoute(withProxy, { enabled: null, own })).toEqual({
|
|
41
|
+
expect(chatRoute(withProxy, { enabled: null, own })).toEqual({
|
|
42
|
+
kind: "own",
|
|
43
|
+
model: own,
|
|
44
|
+
});
|
|
28
45
|
});
|
|
29
46
|
|
|
30
47
|
it("stays off without a proxy until switched on, then asks to be configured", () => {
|
|
31
|
-
expect(chatRoute(noProxy, { enabled: null, own: null })).toEqual({
|
|
32
|
-
|
|
48
|
+
expect(chatRoute(noProxy, { enabled: null, own: null })).toEqual({
|
|
49
|
+
kind: "off",
|
|
50
|
+
});
|
|
51
|
+
expect(chatRoute(noProxy, { enabled: true, own: null })).toEqual({
|
|
52
|
+
kind: "unconfigured",
|
|
53
|
+
});
|
|
33
54
|
});
|
|
34
55
|
|
|
35
56
|
it("does not count a half-filled model as one", () => {
|
|
36
57
|
expect(ownReady({ baseUrl: "", apiKey: "k", model: "m" })).toBe(false);
|
|
37
|
-
expect(ownReady({ baseUrl: "https://a", apiKey: "", model: "m" })).toBe(
|
|
38
|
-
|
|
58
|
+
expect(ownReady({ baseUrl: "https://a", apiKey: "", model: "m" })).toBe(
|
|
59
|
+
true,
|
|
60
|
+
);
|
|
61
|
+
expect(
|
|
62
|
+
chatRoute(noProxy, {
|
|
63
|
+
enabled: true,
|
|
64
|
+
own: { baseUrl: "https://a", apiKey: "", model: " " },
|
|
65
|
+
}),
|
|
66
|
+
).toEqual({ kind: "unconfigured" });
|
|
39
67
|
});
|
|
40
68
|
});
|
|
41
69
|
|
|
42
70
|
describe("routeLabel", () => {
|
|
43
71
|
it("names the model and the host, or the proxy", () => {
|
|
44
|
-
expect(routeLabel({ kind: "own", model: own })).toBe(
|
|
45
|
-
|
|
72
|
+
expect(routeLabel({ kind: "own", model: own })).toBe(
|
|
73
|
+
"llama · api.groq.com",
|
|
74
|
+
);
|
|
75
|
+
expect(routeLabel({ kind: "proxy", url: "u" })).toBe(
|
|
76
|
+
"default model · demo proxy",
|
|
77
|
+
);
|
|
46
78
|
expect(routeLabel({ kind: "off" })).toBe("");
|
|
47
79
|
});
|
|
48
80
|
});
|
|
@@ -50,8 +82,13 @@ describe("routeLabel", () => {
|
|
|
50
82
|
describe("parsePrefs", () => {
|
|
51
83
|
it("reads the switch and the model, and shrugs at junk", () => {
|
|
52
84
|
expect(parsePrefs("on", null)).toEqual({ enabled: true, own: null });
|
|
53
|
-
expect(parsePrefs("off", "{bad json")).toEqual({
|
|
54
|
-
|
|
85
|
+
expect(parsePrefs("off", "{bad json")).toEqual({
|
|
86
|
+
enabled: false,
|
|
87
|
+
own: null,
|
|
88
|
+
});
|
|
89
|
+
expect(
|
|
90
|
+
parsePrefs(null, JSON.stringify({ baseUrl: "https://a", model: "m" })),
|
|
91
|
+
).toEqual({
|
|
55
92
|
enabled: null,
|
|
56
93
|
own: { baseUrl: "https://a", apiKey: "", model: "m" },
|
|
57
94
|
});
|
package/src/chat/flags.ts
CHANGED
|
@@ -39,7 +39,13 @@ export type ChatRoute =
|
|
|
39
39
|
|
|
40
40
|
export const BUILD: ChatBuild = {
|
|
41
41
|
built: import.meta.env.VITE_CHAT !== "off",
|
|
42
|
-
|
|
42
|
+
// The public build receives the worker URL from CI. In development Vite
|
|
43
|
+
// exposes the same worker through a same-origin proxy, so the real demo chat
|
|
44
|
+
// works on any local port without weakening the worker's CORS allowlist.
|
|
45
|
+
proxyUrl: (
|
|
46
|
+
import.meta.env.VITE_CHAT_PROXY_URL ??
|
|
47
|
+
(import.meta.env.DEV ? "/api/portolan-chat" : "")
|
|
48
|
+
).trim(),
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
const OFF: ChatRoute = { kind: "off" };
|
|
@@ -66,7 +72,7 @@ export function chatRoute(build: ChatBuild, prefs: ChatPrefs): ChatRoute {
|
|
|
66
72
|
export function routeLabel(route: ChatRoute): string {
|
|
67
73
|
switch (route.kind) {
|
|
68
74
|
case "proxy":
|
|
69
|
-
return "demo proxy";
|
|
75
|
+
return "default model · demo proxy";
|
|
70
76
|
case "own": {
|
|
71
77
|
let host = route.model.baseUrl;
|
|
72
78
|
try {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { PaletteItem } from "../lib/palette";
|
|
3
|
+
import { contextQuestions, pageContextFrom } from "./page-context";
|
|
4
|
+
|
|
5
|
+
const flow: PaletteItem = {
|
|
6
|
+
kind: "flow",
|
|
7
|
+
id: "flow.cart-checkout",
|
|
8
|
+
name: "cart-checkout",
|
|
9
|
+
detail: "Checkout",
|
|
10
|
+
path: "/flows/cart-checkout",
|
|
11
|
+
context: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const service: PaletteItem = {
|
|
15
|
+
kind: "service",
|
|
16
|
+
id: "shop.cart",
|
|
17
|
+
name: "cart",
|
|
18
|
+
detail: "shop.cart",
|
|
19
|
+
path: "/c/shop/cart",
|
|
20
|
+
context: "shop",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("pageContextFrom", () => {
|
|
24
|
+
it("turns the exact entity route into model context", () => {
|
|
25
|
+
expect(pageContextFrom([flow], "/flows/cart-checkout")).toEqual({
|
|
26
|
+
kind: "flow",
|
|
27
|
+
id: "flow.cart-checkout",
|
|
28
|
+
title: "Checkout",
|
|
29
|
+
docPath: "flows/cart-checkout.md",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not mistake an index or nested route for the entity", () => {
|
|
34
|
+
expect(pageContextFrom([flow], "/flows")).toBeNull();
|
|
35
|
+
expect(pageContextFrom([flow], "/flows/cart-checkout/extra")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("points a service at the markdown path from llms.txt", () => {
|
|
39
|
+
expect(pageContextFrom([service], "/c/shop/cart")?.docPath).toBe(
|
|
40
|
+
"shop/cart/README.md",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("contextQuestions", () => {
|
|
46
|
+
it("keeps every flow starter anchored to the catalog id", () => {
|
|
47
|
+
const page = pageContextFrom([flow], "/flows/cart-checkout");
|
|
48
|
+
expect(page).not.toBeNull();
|
|
49
|
+
expect(contextQuestions(page!).every((question) => question.includes(flow.id))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|