diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fcaefab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## 0.3.0 + +Built on refkit core 0.10 and the provider releases that go with it; they come in as dependencies, nothing to install by hand. + +- **24 sources.** The new `polyhaven-hdri` source searches Poly Haven's HDRIs (no key needed), next to `polyhaven` for its textures. +- **Durations.** Audio and video results carry `durationSec`. The card shows it as an `m:ss` badge, and the result text the model reads names it after the source (`freesound — 1:05`). The sources that report a length are openverse-audio, freesound, jamendo, pexels-video and pixabay-video. +- **Album art and video posters on the card.** A video tile shows its poster under a ▶ mark, never the video file. An audio tile shows its album art and falls back to ♪ when there is none. +- **Honest sizes.** `width` and `height` are the original's size. When a source only knows the size of its preview (flickr, pixabay-video), the tile uses that, so it keeps its aspect ratio. +- New export `RETIRED_SOURCE_IDS`: source ids that once shipped and stay valid in `sources` (empty today). +- Settings, the tool description and the docs say "source ids" instead of "provider ids" (dsh uses "provider" for LLM providers). + +### Breaking + +- The `PROVIDER_IDS` export is now `SOURCE_IDS`. +- In the `refkit_search` output, each reference names its source in `source` instead of `provider`. Cards saved in conversations by 0.2.0 still render. +- With `explain`, `meta` is core 0.10's search metadata and uses its key names: `sources` (was `providers`), `sourceId` in each entry (was `providerId`), and `controls.appliedBySource` / `ignoredBySource` (were `appliedByProvider` / `ignoredByProvider`). +- Requires `@refkit/core` 0.10. A client built with an older core, for example one passed to `applyWith`, no longer fits. + +## 0.2.0 + +- Ported to dsh 0.1.7 (requires `^0.1.7-rc.2`). Settings are live: an edit applies on the next call without a restart, and an unknown `sources` id is refused when saving. +- A settings page on dsh's Plugins page (Plugins (sidebar) → refkit → Components → refkit) for the API keys, which are write-only and shown as Set / Not set, and for search tuning. +- Display metadata on the Plugins page: title, English and Chinese description, icon. +- The card follows the call through preparing, searching and the result. Wikimedia Commons thumbnails are 500 px. +- The first version published to npm. + +## 0.1.0 + +- First release, for dsh 0.1.5-rc.2. `refkit_search` searches up to 23 sources, normalizes their licenses, and can annotate each result with a use-verdict and a credit line, or keep only the usable ones. `refkit_rights` re-checks one license for another intent. A web card shows license chips and verdict badges. Keys come from the settings or from the same environment variables as `@refkit/mcp`. diff --git a/README.md b/README.md index b0e0811..2582b15 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ English | [中文](README.zh.md) [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin for [refkit](https://github.com/refkitjs/refkit): license-normalized creative reference search as native agent tools, with a web card that shows the license and the use-verdict on every result. -- **`refkit_search`** — one call fans out to up to 23 sources (Openverse, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Smithsonian, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven, ambientCG, Europeana, Unsplash, Pexels, Pixabay, Flickr, Freesound, Jamendo, Brave, nailbook), merges and reranks the results, and returns each with its license id, canonical link, and — when you pass an `intent` — a use-verdict and a ready credit line. +- **`refkit_search`** — one call fans out to up to 24 sources (Openverse images and audio, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Smithsonian, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven textures and HDRIs, ambientCG, Europeana, Unsplash, Pexels, Pixabay, Flickr, Freesound, Jamendo, Brave, nailbook), merges and reranks the results, and returns each with its source id, license id, canonical link, the length of audio and video clips where the source reports it, and — when you pass an `intent` — a use-verdict and a ready credit line. - **`refkit_rights`** — re-check one license for a different intent without searching again. -- **Web card** — thumbnail grid; license chip on every tile; green / blue / red / amber badges for allowed / credit required / not allowed / needs review; one-click credit copy. +- **Web card** — thumbnail grid; source and license chips on every tile; video posters with a play mark, album art on audio, and an `m:ss` length badge on both; green / blue / red / amber badges for allowed / credit required / not allowed / needs review; one-click credit copy. -Eleven sources work with no key at all (Openverse, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven, ambientCG, nailbook). Add free keys for the rest under **Plugins (sidebar) → refkit → Components → refkit**. +Thirteen of the 24 sources work with no key at all (Openverse images and audio, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven textures and HDRIs, ambientCG, nailbook). Add free keys for the rest under **Plugins (sidebar) → refkit → Components → refkit**. ## Install @@ -39,14 +39,14 @@ Keys and tuning are set on **Plugins (sidebar) → refkit → Components → ref | Field | Default | Meaning | | --- | --- | --- | -| `sources` | `[]` | Provider ids to enable; empty = every source whose key is present | -| `limit` | `12` | Default results per call (1–30); also caps per-item detail fetches for met, rijksmuseum, polyhaven | +| `sources` | `[]` | Source ids to enable; empty = every source whose key is present | +| `limit` | `12` | Default results per call (1–30); also caps per-item detail fetches for met, rijksmuseum, polyhaven, polyhaven-hdri | | `poolFactor` | `2` | Rank-fusion pool multiplier (1–4) | | `deadlineMs` | `15000` | Whole-search deadline | | `timeoutMs` | `10000` | Per-source timeout | | `rerank` | `true` | Lexical reranker over title, description, tags and excerpt (CJK aware) | | `sourceConfidence` | `true` | Down-weight sources whose batch never mentions the query | -| `userAgent` | `refkit-dsh-plugin/` | Sent with provider requests | +| `userAgent` | `refkit-dsh-plugin/` | Sent with source requests | ## Tools @@ -58,14 +58,16 @@ Keys and tuning are set on **Plugins (sidebar) → refkit → Components → ref | `modalities` | `image` `video` `audio` `text` | Default `["image"]` | | `intent` | `internal-moodboard` `commercial-product` `ai-generation-input` `redistribution` | Annotate every result with a use-verdict and credit line | | `gateFor` | same | Return only results whose license allows the intent | -| `sources` | string[] | Restrict to provider ids | +| `sources` | string[] | Restrict to source ids (`polyhaven-hdri` for HDRIs, `openverse-audio` for audio, …) | | `limit` | 1–30 | Default from configuration | | `cursor` | string | Continuation from a previous `nextCursor` | | `controls` | object | orientation, color, language, sort, safety, license, media, creator, text, page | | `minRelevance` | 0–1 | Drop results the reranker scored below this | | `explain` | boolean | Include per-source diagnostics under `meta` | -Example prompts: “find me reference photos of brutalist libraries I can use in a commercial pitch deck”, “给我找几张可以商用的赛博朋克街景参考图”, “a public-domain poem about the sea for a poster”. +Each result names its `source` id and carries its license, canonical link, thumbnail (a video's poster, a track's album art) and, for audio and video when the source reports it, `durationSec`; `width`/`height` are the original's size, or the preview's when the source does not report it. + +Example prompts: “find me reference photos of brutalist libraries I can use in a commercial pitch deck”, “给我找几张可以商用的赛博朋克街景参考图”, “a public-domain poem about the sea for a poster”, “an outdoor HDRI for lighting a product render”, “a short rain sound effect for a game trailer”. ### `refkit_rights` diff --git a/README.zh.md b/README.zh.md index e0480a1..b0efc13 100644 --- a/README.zh.md +++ b/README.zh.md @@ -4,11 +4,11 @@ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)插件,封装了 [refkit](https://github.com/refkitjs/refkit) 参考搜索库:将经过许可证归一化处理的创意素材搜索能力,变成原生 agent 工具,并配有网页卡片,在每条结果上展示许可证与使用判定。 -- **`refkit_search`** — 一次调用即可扇出到多达 23 个来源(Openverse、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Smithsonian、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven、ambientCG、Europeana、Unsplash、Pexels、Pixabay、Flickr、Freesound、Jamendo、Brave、nailbook),合并并重新排序结果,并为每条结果返回其许可证 id、canonical 链接,以及——当你传入 `intent` 时——使用判定和一条可直接使用的署名文本。 +- **`refkit_search`** — 一次调用即可扇出到多达 24 个来源(Openverse 图片与音频、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Smithsonian、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven 纹理与 HDRI、ambientCG、Europeana、Unsplash、Pexels、Pixabay、Flickr、Freesound、Jamendo、Brave、nailbook),合并并重新排序结果,并为每条结果返回其来源 id、许可证 id、canonical 链接、音频和视频的时长(来源提供时),以及——当你传入 `intent` 时——使用判定和一条可直接使用的署名文本。 - **`refkit_rights`** — 针对不同的使用意图重新校验同一条许可证,无需重新搜索。 -- **网页卡片** — 缩略图网格;每个卡片都带许可证标签;绿 / 蓝 / 红 / 黄四色徽章分别对应 allowed(允许) / credit required(需署名) / not allowed(不允许) / needs review(需人工复核);一键复制署名文本。 +- **网页卡片** — 缩略图网格;每个卡片都带来源和许可证标签;视频显示封面帧并叠加播放标记,音频显示专辑封面,两者都带 `m:ss` 时长徽章;绿 / 蓝 / 红 / 黄四色徽章分别对应 allowed(允许) / credit required(需署名) / not allowed(不允许) / needs review(需人工复核);一键复制署名文本。 -十一个来源无需任何密钥即可使用(Openverse、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven、ambientCG、nailbook)。其余来源在「插件(侧边栏)→ refkit → 包含的组件 → refkit」里填入免费密钥即可启用。 +24 个来源中有 13 个无需任何密钥即可使用(Openverse 图片与音频、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven 纹理与 HDRI、ambientCG、nailbook)。其余来源在「插件(侧边栏)→ refkit → 包含的组件 → refkit」里填入免费密钥即可启用。 ## 安装 @@ -40,7 +40,7 @@ dsh plugin --profile web add github:refkitjs/dsh-plugin | 字段 | 默认值 | 含义 | | --- | --- | --- | | `sources` | `[]` | 要启用的来源 id;为空 = 启用所有已配置 key 的来源 | -| `limit` | `12` | 每次调用默认返回的结果数(1–30);也限制 met、rijksmuseum、polyhaven 的逐条详情拉取数量 | +| `limit` | `12` | 每次调用默认返回的结果数(1–30);也限制 met、rijksmuseum、polyhaven、polyhaven-hdri 的逐条详情拉取数量 | | `poolFactor` | `2` | 排序融合(rank-fusion)的候选池倍数(1–4) | | `deadlineMs` | `15000` | 整次搜索的截止时间 | | `timeoutMs` | `10000` | 单个来源的超时时间 | @@ -58,14 +58,16 @@ dsh plugin --profile web add github:refkitjs/dsh-plugin | `modalities` | `image` `video` `audio` `text` | 默认 `["image"]` | | `intent` | `internal-moodboard` `commercial-product` `ai-generation-input` `redistribution` | 为每条结果附加使用判定和署名文本 | | `gateFor` | 同上 | 仅返回许可证允许该用途的结果 | -| `sources` | string[] | 限定到特定的来源 id | +| `sources` | string[] | 限定到特定的来源 id(例如 HDRI 用 `polyhaven-hdri`,音频用 `openverse-audio`) | | `limit` | 1–30 | 默认取自配置 | | `cursor` | string | 沿用上一次结果里的 `nextCursor` 继续翻页 | | `controls` | object | orientation、color、language、sort、safety、license、media、creator、text、page | | `minRelevance` | 0–1 | 丢弃重排序分数低于该值的结果 | | `explain` | boolean | 在 `meta` 下附带每个来源的诊断信息 | -示例提示词:“find me reference photos of brutalist libraries I can use in a commercial pitch deck”、“给我找几张可以商用的赛博朋克街景参考图”、“a public-domain poem about the sea for a poster”。 +每条结果都标明其来源 id(`source`),并带有许可证、canonical 链接、缩略图(视频的封面帧、音轨的专辑封面),音频和视频在来源提供时还带有 `durationSec`(时长,秒);`width`/`height` 是原件尺寸,来源未提供时取预览文件的尺寸。 + +示例提示词:“find me reference photos of brutalist libraries I can use in a commercial pitch deck”、“给我找几张可以商用的赛博朋克街景参考图”、“a public-domain poem about the sea for a poster”、“找一张户外 HDRI 用来给产品渲染打光”、“一段用于游戏预告片的短雨声音效”。 ### `refkit_rights` diff --git a/cordis.patch.yml b/cordis.patch.yml index e3c50b1..0d4c022 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -8,7 +8,7 @@ - id: refkit name: '@refkit/dsh-plugin' config: - # sources: [] # whitelist of provider ids; empty = every enabled source + # sources: [] # whitelist of source ids; empty = every enabled source # limit: 12 # default results per call (1..30); also caps museum detail fetches # poolFactor: 2 # fusion pool multiplier (1..4) # deadlineMs: 15000 # whole-search deadline diff --git a/lib/client.js b/lib/client.js index e8fcad1..a0e8cde 100644 --- a/lib/client.js +++ b/lib/client.js @@ -39,6 +39,18 @@ window.__ModuleLoader__.load({ function num(v) { return typeof v === "number" && Number.isFinite(v) ? v : void 0; } + /** + * A duration as `m:ss`, or `h:mm:ss` from an hour on, rounded to the nearest second; a + * sub-second clip reads `0:01`, never `0:00`. Undefined for a missing, non-finite or non-positive value. + */ + function formatDuration(sec) { + if (sec === void 0 || !Number.isFinite(sec) || sec <= 0) return void 0; + const total = Math.max(1, Math.round(sec)); + const h = Math.floor(total / 3600); + const m = Math.floor(total % 3600 / 60); + const ss = String(total % 60).padStart(2, "0"); + return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`; + } function narrowVerdict(v) { if (typeof v !== "object" || v === null) return void 0; const r = v; @@ -50,21 +62,24 @@ window.__ModuleLoader__.load({ confidence: r.confidence }; } - /** Soft-narrow one tile; null for anything unusable. Unknown keys are dropped. */ + /** + * Soft-narrow one tile; null for anything unusable. Unknown keys are dropped. A tile persisted by + * 0.2.0 names its source `provider`; it is read as `source` so old conversations still render. + */ function narrowTile(value) { if (typeof value !== "object" || value === null) return null; const r = value; const id = str(r.id); - const provider = str(r.provider); + const source = str(r.source) ?? str(r.provider); const canonicalUrl = str(r.canonicalUrl); const license = str(r.license); - if (id === void 0 || provider === void 0 || license === void 0) return null; + if (id === void 0 || source === void 0 || license === void 0) return null; if (canonicalUrl === void 0 || !HTTP.test(canonicalUrl)) return null; if (!MODALITIES.includes(r.modality)) return null; const tile = { id, modality: r.modality, - provider, + source, canonicalUrl, license }; @@ -84,7 +99,11 @@ window.__ModuleLoader__.load({ const s = str(r[key]); if (s !== void 0 && HTTP.test(s)) tile[key] = s; } - for (const key of ["width", "height"]) { + for (const key of [ + "width", + "height", + "durationSec" + ]) { const n = num(r[key]); if (n !== void 0 && n > 0) tile[key] = n; } @@ -218,7 +237,7 @@ window.__ModuleLoader__.load({ removeFailed: "Not removed; try again.", searchHeading: "Search", limit: "Results per call", - limitHint: "Also caps per-item detail fetches for met, rijksmuseum and polyhaven.", + limitHint: "Also caps per-item detail fetches for met, rijksmuseum, polyhaven and polyhaven-hdri.", poolFactor: "Fusion pool factor", poolFactorHint: "Rank-fusion pool multiplier.", deadlineMs: "Search deadline (ms)", @@ -228,12 +247,12 @@ window.__ModuleLoader__.load({ range: (hint, bounds) => `${hint} ${bounds.min}–${bounds.max}, default ${bounds.default}.`, invalidRange: (bounds) => `Enter a whole number from ${bounds.min} to ${bounds.max}, or leave blank for the default.`, sources: "Sources", - sourcesHint: "Comma-separated provider ids. A keyed source also needs its key.", + sourcesHint: "Comma-separated source ids. A keyed source also needs its key.", sourcesPlaceholder: "empty = all enabled sources", sourcesHelp: "Source ids", sourcesInvalid: "Unknown source id. Use the ids listed under the info button.", userAgent: "User-Agent", - userAgentHint: "Sent with provider requests. Blank uses refkit-dsh-plugin/.", + userAgentHint: "Sent with source requests. Blank uses refkit-dsh-plugin/.", rerank: "Lexical rerank", rerankHint: "Rerank fused results over title, description, tags and excerpt.", sourceConfidence: "Source confidence", @@ -262,6 +281,9 @@ window.__ModuleLoader__.load({ .rk-media img { display: block; width: 100%; height: 100%; object-fit: cover; } .rk-media-text { padding: 10px; font-size: 12px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; white-space: pre-wrap; } .rk-media-glyph { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 28px; color: var(--dsw-alias-label-tertiary, #8c959f); } +.rk-media img.rk-cover { position: absolute; inset: 0; } +.rk-play { position: absolute; top: 50%; left: 50%; width: 34px; height: 34px; margin: -17px 0 0 -17px; padding-left: 3px; box-sizing: border-box; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #fff; background: rgba(0,0,0,.55); pointer-events: none; } +.rk-duration { position: absolute; top: 6px; right: 6px; padding: 1px 6px; border-radius: 4px; font-size: 10.5px; font-weight: 600; line-height: 16px; color: #fff; background: rgba(0,0,0,.62); font-variant-numeric: tabular-nums; } .rk-badge { position: absolute; top: 6px; left: 6px; padding: 1px 7px; border-radius: 999px; font-size: 10.5px; font-weight: 600; line-height: 16px; color: #fff; } .rk-badge-allowed { background: #1a7f37; } .rk-badge-attribution { background: #0969da; } @@ -313,9 +335,10 @@ window.__ModuleLoader__.load({ /** * Keyed tool.call.toolview entry for refkit_search. Parses the settled block's * presentation metadata into a SearchOutcome and renders a thumbnail grid: - * license chip on every tile, a coloured use-verdict badge when the call - * carried an intent, and a one-click credit copy. Every failure path degrades - * to plain text — the chat row never breaks. + * source and license chips on every tile, a video's poster under a play mark, + * an audio track's album art, an `m:ss` badge on audio and video, a coloured + * use-verdict badge when the call carried an intent, and a one-click credit + * copy. Every failure path degrades to plain text — the chat row never breaks. * @module @refkit/dsh-plugin/client/Card */ ensureStyle(); @@ -377,46 +400,83 @@ window.__ModuleLoader__.load({ children: state === "copied" ? COPY.copied : COPY.copyCredit }); } - function Tile({ tile }) { - const image = tile.thumbnail ?? tile.preview; - const badge = tile.useVerdict ? verdictBadge(tile.useVerdict.decision) : null; - let media; - if (tile.modality === "image" && image) media = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", { - src: image, - alt: tile.title ?? "", + /** A hot-link-blocked image hides itself; the sized rk-media box, and any glyph or play mark under it, remain. */ + function hideOnError(e) { + e.currentTarget.style.display = "none"; + } + /** A poster or album art: decorative, since the title already shows in `.rk-title`. */ + function Cover({ src }) { + return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", { + className: "rk-cover", + src, + alt: "", loading: "lazy", referrerPolicy: "no-referrer", - onError: (e) => { - e.currentTarget.style.display = "none"; - } - }); - else if (tile.modality === "text") media = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { - className: "rk-media-text", - children: tile.excerpt ?? tile.description ?? tile.title ?? COPY.untitled - }); - else media = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { - className: "rk-media-glyph", - "aria-label": tile.modality, - children: tile.modality === "audio" ? "♪" : tile.modality === "video" ? "▶" : "▦" + onError: hideOnError }); + } + function Media({ tile }) { + const alt = tile.title ?? ""; + switch (tile.modality) { + case "image": { + const image = tile.thumbnail ?? tile.preview; + return image ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", { + src: image, + alt, + loading: "lazy", + referrerPolicy: "no-referrer", + onError: hideOnError + }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { + className: "rk-media-glyph", + "aria-label": "image", + children: "▦" + }); + } + case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { + className: "rk-media-text", + children: tile.excerpt ?? tile.description ?? tile.title ?? COPY.untitled + }); + case "video": return tile.thumbnail ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cover, { src: tile.thumbnail }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { + className: "rk-play", + "aria-hidden": "true", + children: "▶" + })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { + className: "rk-media-glyph", + "aria-label": "video", + children: "▶" + }); + case "audio": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { + className: "rk-media-glyph", + "aria-label": "audio", + children: "♪" + }), tile.thumbnail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cover, { src: tile.thumbnail })] }); + } + } + function Tile({ tile }) { + const badge = tile.useVerdict ? verdictBadge(tile.useVerdict.decision) : null; + const duration = tile.modality === "audio" || tile.modality === "video" ? formatDuration(tile.durationSec) : void 0; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: "rk-tile", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: "rk-media", style: { aspectRatio: aspectRatio(tile) }, children: [ - media, + /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Media, { tile }), badge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: badge.className, title: tile.useVerdict?.reason, children: badge.label }), + duration && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { + className: "rk-duration", + children: duration + }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: "rk-chips", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "rk-chip", - title: tile.provider, - children: tile.provider + title: tile.source, + children: tile.source }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "rk-chip", title: tile.license, @@ -537,7 +597,7 @@ window.__ModuleLoader__.load({ //#region src/client/settings-model.ts /** The Host settings namespace: the plugin's row id in `cordis.patch.yml`. */ const SETTINGS_NS = "refkit"; - /** Secret field → the providers it enables. Key order is the page's display order. */ + /** Secret field → the sources it enables. Key order is the page's display order. */ const KEY_LABELS = { unsplashAccessKey: "Unsplash (images)", pexelsApiKey: "Pexels (images, video)", @@ -565,7 +625,7 @@ window.__ModuleLoader__.load({ europeanaApiKey: ["REFKIT_EUROPEANA_KEY", "EUROPEANA_KEY"], openverseToken: ["REFKIT_OPENVERSE_TOKEN"] }; - /** Every provider id `sources` accepts (mirrors `PROVIDER_IDS`). */ + /** Every live source id `sources` accepts (mirrors the host's `SOURCE_IDS` in src/config.ts). */ const SOURCE_IDS = [ "artic", "brave", @@ -585,6 +645,7 @@ window.__ModuleLoader__.load({ "pixabay-video", "poetrydb", "polyhaven", + "polyhaven-hdri", "ambientcg", "rijksmuseum", "smithsonian", @@ -592,7 +653,7 @@ window.__ModuleLoader__.load({ "wikimedia-commons" ]; /** - * Ids that once shipped and were since removed (mirrors `RETIRED_IDS`). `sources` still accepts + * Ids that once shipped and were since removed (mirrors the host's `RETIRED_SOURCE_IDS`). `sources` still accepts * them, so a stored one never blocks saving the page, but the page does not list them. */ const RETIRED_SOURCE_IDS = []; @@ -644,7 +705,7 @@ window.__ModuleLoader__.load({ }; } /** - * A provider-id list staged as comma-separated text. Ids are matched + * A source-id list staged as comma-separated text. Ids are matched * case-insensitively and deduplicated in order; an unknown id makes the draft * invalid, which blocks the save. A blank draft clears the field (every * enabled source). diff --git a/lib/client.js.map b/lib/client.js.map index ec64f1c..1a38228 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1 +1 @@ -{"version":3,"file":"client.js","names":["SettingsFormModel","SettingsForm","SettingsValueField","SettingsSecretField","Button","Tag","Switch"],"sources":["../src/core/outcome.ts","../src/client/badges.ts","../src/client/copy.ts","../src/client/styles.ts","../src/client/Card.tsx","../src/client/settings-model.ts","../src/client/settings-controller.ts","../src/client/SettingsPage.tsx","../src/client/index.tsx"],"sourcesContent":["/**\n * Canonical result vocabulary shared by the host half (tool output, render,\n * presentation metadata) and the browser half (the card). No runtime\n * dependencies: plain data plus soft parsers, so a malformed or\n * version-drifted payload degrades to text instead of crashing a render.\n * @module @refkit/dsh-plugin/core\n */\n\nexport const MODALITIES = ['image', 'video', 'audio', 'text'] as const\nexport type Modality = (typeof MODALITIES)[number]\n\nexport const DECISIONS = ['allowed', 'allowed-with-attribution', 'denied', 'needs-review'] as const\nexport type Decision = (typeof DECISIONS)[number]\n\nexport const SOURCE_STATUSES = ['fulfilled', 'failed', 'skipped'] as const\nexport type SourceStatusKind = (typeof SOURCE_STATUSES)[number]\n\n/** Lossless JSON. */\nexport type Json = string | number | boolean | null | Json[] | { [key: string]: Json }\n\nexport interface VerdictSummary {\n decision: Decision\n reason: string\n confidence: 'high' | 'low'\n}\n\n/** One reference as the tool returns it and the card renders it. */\nexport interface RefTile {\n id: string\n modality: Modality\n provider: string\n canonicalUrl: string\n license: string\n title?: string\n kind?: string\n licenseVersion?: string\n author?: string\n thumbnail?: string\n preview?: string\n width?: number\n height?: number\n description?: string\n excerpt?: string\n tags?: string[]\n useVerdict?: VerdictSummary\n attribution?: string\n}\n\nexport interface SourceStatus {\n id: string\n status: SourceStatusKind\n reason?: string\n returned?: number\n}\n\n/** The canonical value of one refkit_search call. */\nexport interface SearchOutcome {\n query: string\n modalities: Modality[]\n intent?: string\n count: number\n references: RefTile[]\n nextCursor?: string\n sources: SourceStatus[]\n warnings: string[]\n note?: string\n /** Core's full SearchMeta, only when the caller passed `explain`. */\n meta?: Json\n}\n\n/** What the card receives: the outcome minus `meta`, descriptions cut, tags dropped. */\nexport type CardOutcome = Omit\n\nconst HTTP = /^https?:\\/\\//i\n\nfunction str(v: unknown): string | undefined {\n return typeof v === 'string' && v.length > 0 ? v : undefined\n}\nfunction num(v: unknown): number | undefined {\n return typeof v === 'number' && Number.isFinite(v) ? v : undefined\n}\n\n/** Shallow copy of `obj` without its undefined-valued own properties (dsh snapshots values as lossless JSON). */\nexport function defined(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T\n}\n\n/** Cut `text` to `max` code points, appending an ellipsis when anything was removed. */\nexport function trunc(text: string, max: number): string {\n const points = Array.from(text)\n if (points.length <= max) return text\n return points.slice(0, Math.max(0, max - 1)).join('') + '…'\n}\n\nfunction narrowVerdict(v: unknown): VerdictSummary | undefined {\n if (typeof v !== 'object' || v === null) return undefined\n const r = v as Record\n if (!DECISIONS.includes(r.decision as Decision)) return undefined\n if (r.confidence !== 'high' && r.confidence !== 'low') return undefined\n return { decision: r.decision as Decision, reason: str(r.reason) ?? '', confidence: r.confidence }\n}\n\n/** Soft-narrow one tile; null for anything unusable. Unknown keys are dropped. */\nexport function narrowTile(value: unknown): RefTile | null {\n if (typeof value !== 'object' || value === null) return null\n const r = value as Record\n const id = str(r.id)\n const provider = str(r.provider)\n const canonicalUrl = str(r.canonicalUrl)\n const license = str(r.license)\n if (id === undefined || provider === undefined || license === undefined) return null\n if (canonicalUrl === undefined || !HTTP.test(canonicalUrl)) return null\n if (!MODALITIES.includes(r.modality as Modality)) return null\n const tile: RefTile = { id, modality: r.modality as Modality, provider, canonicalUrl, license }\n for (const key of ['title', 'kind', 'licenseVersion', 'author', 'description', 'excerpt', 'attribution'] as const) {\n const s = str(r[key])\n if (s !== undefined) tile[key] = s\n }\n for (const key of ['thumbnail', 'preview'] as const) {\n const s = str(r[key])\n if (s !== undefined && HTTP.test(s)) tile[key] = s\n }\n for (const key of ['width', 'height'] as const) {\n const n = num(r[key])\n if (n !== undefined && n > 0) tile[key] = n\n }\n if (Array.isArray(r.tags)) {\n const tags = r.tags.filter((t): t is string => typeof t === 'string' && t.length > 0)\n if (tags.length > 0) tile.tags = tags\n }\n const verdict = narrowVerdict(r.useVerdict)\n if (verdict !== undefined) tile.useVerdict = verdict\n return tile\n}\n\nfunction narrowSource(v: unknown): SourceStatus | null {\n if (typeof v !== 'object' || v === null) return null\n const r = v as Record\n const id = str(r.id)\n if (id === undefined || !SOURCE_STATUSES.includes(r.status as SourceStatusKind)) return null\n const out: SourceStatus = { id, status: r.status as SourceStatusKind }\n const reason = str(r.reason)\n if (reason !== undefined) out.reason = reason\n const returned = num(r.returned)\n if (returned !== undefined) out.returned = returned\n return out\n}\n\n/** Soft-parse a canonical value or presentation metadata; null for the wrong shape. */\nexport function narrowOutcome(value: unknown): SearchOutcome | null {\n if (typeof value !== 'object' || value === null) return null\n const r = value as Record\n const query = typeof r.query === 'string' ? r.query : undefined\n if (query === undefined || typeof r.count !== 'number') return null\n if (!Array.isArray(r.modalities) || !Array.isArray(r.references) || !Array.isArray(r.sources)) return null\n if (!Array.isArray(r.warnings)) return null\n const modalities = r.modalities.filter((m): m is Modality => MODALITIES.includes(m as Modality))\n const references = r.references.map(narrowTile).filter((t): t is RefTile => t !== null)\n const sources = r.sources.map(narrowSource).filter((s): s is SourceStatus => s !== null)\n const warnings = r.warnings.filter((w): w is string => typeof w === 'string')\n const out: SearchOutcome = { query, modalities, count: r.count, references, sources, warnings }\n const intent = str(r.intent)\n if (intent !== undefined) out.intent = intent\n const nextCursor = str(r.nextCursor)\n if (nextCursor !== undefined) out.nextCursor = nextCursor\n const note = str(r.note)\n if (note !== undefined) out.note = note\n if (r.meta !== undefined) out.meta = r.meta as Json\n return out\n}\n","/**\n * Pure presentation helpers for the card: verdict badge classes and labels,\n * license chip text, source bucketing, tile aspect ratio. No DOM, no React,\n * so they are unit-tested directly.\n * @module @refkit/dsh-plugin/client/badges\n */\n\nimport type { Decision, RefTile, SourceStatus } from '../core/outcome.ts'\n\nconst LICENSE_CHARS = 20\n\nexport function verdictBadge(decision: Decision): { className: string; label: string } {\n switch (decision) {\n case 'allowed': return { className: 'rk-badge rk-badge-allowed', label: 'Allowed' }\n case 'allowed-with-attribution': return { className: 'rk-badge rk-badge-attribution', label: 'Credit required' }\n case 'denied': return { className: 'rk-badge rk-badge-denied', label: 'Not allowed' }\n case 'needs-review': return { className: 'rk-badge rk-badge-review', label: 'Needs review' }\n }\n}\n\nexport function licenseLabel(tile: Pick): string {\n const base = tile.licenseVersion ? `${tile.license} ${tile.licenseVersion}` : tile.license\n const points = Array.from(base)\n return points.length > LICENSE_CHARS ? points.slice(0, LICENSE_CHARS).join('') + '…' : base\n}\n\nexport function summarizeSources(sources: readonly SourceStatus[]): { fulfilled: SourceStatus[]; failed: SourceStatus[]; skipped: SourceStatus[] } {\n return {\n fulfilled: sources.filter(s => s.status === 'fulfilled'),\n failed: sources.filter(s => s.status === 'failed'),\n skipped: sources.filter(s => s.status === 'skipped'),\n }\n}\n\n/** CSS `aspect-ratio` value for a tile. */\nexport function aspectRatio(tile: Pick): string {\n return tile.width && tile.height && tile.width > 0 && tile.height > 0 ? `${tile.width} / ${tile.height}` : '4 / 3'\n}\n","/** UI strings for the card and the settings page. One object so a locale swap is one file. */\nexport const COPY = {\n preparing: 'Preparing refkit search…',\n searching: 'Searching refkit sources…',\n refsFor: (count: number, query: string) => `${count} reference${count === 1 ? '' : 's'} for “${query}”`,\n intent: (intent: string) => `intent: ${intent}`,\n more: 'more available — ask for the next page',\n failed: (n: number) => `${n} source${n === 1 ? '' : 's'} failed`,\n skipped: (n: number) => `${n} skipped`,\n open: 'Open',\n copyCredit: 'Copy credit',\n copied: 'Copied',\n copyFailed: 'Select and copy:',\n empty: 'No results. Try broader terms, another modality, or fewer controls.',\n legend: 'Verdict:',\n untitled: '(untitled)',\n settings: {\n summary: (configured: number, total: number) => `License-aware reference search · ${configured} of ${total} keys set`,\n // The shared form frame.\n unavailable: 'refkit is not running, so it cannot be configured right now.',\n readOnly: 'This deployment stores settings read-only.',\n saveFailed: 'The deployment did not accept these values; they were left for you to correct.',\n save: 'Save',\n saving: 'Saving…',\n overridden: 'Overridden',\n reset: 'Reset to default',\n // API keys.\n keysHeading: 'API keys',\n keysNote: 'Each key enables the sources named beside it; keyless sources need none. Keys are stored in plain text in this profile’s cordis.patch.yml (file mode 0600) and apply on the next search.',\n keyHint: (env: readonly string[]) => `Leave blank to keep the stored key. Environment fallback: ${env.join(' / ')}.`,\n keySet: 'Set',\n keyUnset: 'Not set',\n remove: 'Remove',\n removeLabel: (label: string) => `Remove the ${label} key`,\n removing: 'Removing…',\n removeBlocked: 'Save your other edits first: removing a key writes immediately.',\n removeFailed: 'Not removed; try again.',\n // Search tuning.\n searchHeading: 'Search',\n limit: 'Results per call',\n limitHint: 'Also caps per-item detail fetches for met, rijksmuseum and polyhaven.',\n poolFactor: 'Fusion pool factor',\n poolFactorHint: 'Rank-fusion pool multiplier.',\n deadlineMs: 'Search deadline (ms)',\n deadlineMsHint: 'Whole-search deadline.',\n timeoutMs: 'Per-source timeout (ms)',\n timeoutMsHint: 'How long one source may take.',\n range: (hint: string, bounds: { min: number; max: number; default: number }) => `${hint} ${bounds.min}–${bounds.max}, default ${bounds.default}.`,\n invalidRange: (bounds: { min: number; max: number }) => `Enter a whole number from ${bounds.min} to ${bounds.max}, or leave blank for the default.`,\n sources: 'Sources',\n sourcesHint: 'Comma-separated provider ids. A keyed source also needs its key.',\n sourcesPlaceholder: 'empty = all enabled sources',\n sourcesHelp: 'Source ids',\n sourcesInvalid: 'Unknown source id. Use the ids listed under the info button.',\n userAgent: 'User-Agent',\n userAgentHint: 'Sent with provider requests. Blank uses refkit-dsh-plugin/.',\n rerank: 'Lexical rerank',\n rerankHint: 'Rerank fused results over title, description, tags and excerpt.',\n sourceConfidence: 'Source confidence',\n sourceConfidenceHint: 'Down-weight sources whose batch never mentions the query.',\n },\n} as const\n","/**\n * Card and settings-page stylesheet, injected once as