From fbec6e9373d0375d7df18316ae41163808cc81ad Mon Sep 17 00:00:00 2001 From: lizhixuan Date: Thu, 22 Oct 2026 23:11:49 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat!:=20adapt=20to=20refkit=20core=200.10?= =?UTF-8?q?=20=E2=80=94=20sources=20naming,=20modality=20union,=20duration?= =?UTF-8?q?s,=20HDRIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deps: @refkit/core ^0.10.0; artic, brave, gutendex, met, pexels, pixabay, poetrydb, polyhaven, rijksmuseum, smithsonian, unsplash ^0.5.0; europeana, flickr, freesound, internet-archive, jamendo, openverse, wikimedia-commons ^0.6.0; nailbook ^0.3.0 - new source polyhaven-hdri (Poly Haven HDRIs); 24 sources - core D4 names: meta.sources / sourceId, onSourceError, ref.source.id; the selection miss is detected with isSourceSelectionError and code 'no-matching-source' instead of matching core's message - tile: `provider` -> `source` (narrowTile still reads 0.2.0's `provider`), new durationSec, width/height fall back to the preview rendition - card: video poster under a play mark (never the video file), audio album art over the note glyph, m:ss duration badge; render names the duration - PROVIDER_IDS -> SOURCE_IDS, RETIRED_IDS -> RETIRED_SOURCE_IDS (both now exported); "provider ids" copy -> "source ids" --- README.md | 18 +- README.zh.md | 14 +- cordis.patch.yml | 2 +- lib/client.js | 140 ++++++++++---- lib/client.js.map | 2 +- lib/index.js | 111 +++++++---- lib/types/client/Card.d.ts | 7 +- lib/types/client/Card.d.ts.map | 2 +- lib/types/client/badges.d.ts | 6 +- lib/types/client/badges.d.ts.map | 2 +- lib/types/client/copy.d.ts | 6 +- lib/types/client/settings-model.d.ts | 8 +- lib/types/client/settings-model.d.ts.map | 2 +- lib/types/client/styles.d.ts | 2 +- lib/types/client/styles.d.ts.map | 2 +- lib/types/config.d.ts | 25 +-- lib/types/config.d.ts.map | 2 +- lib/types/core/outcome.d.ts | 16 +- lib/types/core/outcome.d.ts.map | 2 +- lib/types/index.d.ts | 2 +- lib/types/index.d.ts.map | 2 +- lib/types/render.d.ts.map | 2 +- lib/types/tools/search.d.ts | 19 +- lib/types/tools/search.d.ts.map | 2 +- locale/en.json | 2 +- locale/zh.json | 2 +- package.json | 42 ++--- pnpm-lock.yaml | 230 +++++++++++------------ scripts/smoke-host.mjs | 4 +- src/client/Card.tsx | 58 ++++-- src/client/badges.ts | 7 +- src/client/copy.ts | 6 +- src/client/settings-model.ts | 10 +- src/client/styles.ts | 3 + src/config.ts | 32 ++-- src/core/outcome.ts | 32 +++- src/index.ts | 2 +- src/render.ts | 10 +- src/tools/search.ts | 59 +++--- tests/badges.test.ts | 25 ++- tests/card.test.tsx | 61 ++++++ tests/config.test.ts | 57 ++++-- tests/copy.test.ts | 8 + tests/index.test.ts | 7 + tests/outcome.test.ts | 20 +- tests/package-meta.test.ts | 2 +- tests/render.test.ts | 23 ++- tests/search.test.ts | 105 ++++++++++- tests/settings-model.test.ts | 8 +- 49 files changed, 828 insertions(+), 383 deletions(-) diff --git a/README.md b/README.md index b0e0811..b667f0c 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, 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, `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..1f707e6 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..eeae127 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,88 @@ 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"; + } + function Cover({ src, alt }) { + 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, + alt + }), /* @__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, + alt + })] }); + } + } + 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 +602,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 +630,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 +650,7 @@ window.__ModuleLoader__.load({ "pixabay-video", "poetrydb", "polyhaven", + "polyhaven-hdri", "ambientcg", "rijksmuseum", "smithsonian", @@ -592,7 +658,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 +710,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..9878b25 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