diff --git a/README.en.md b/README.en.md index 3c55b9d..f7b8c68 100644 --- a/README.en.md +++ b/README.en.md @@ -4,7 +4,13 @@ Share DSH Q&As or selected conversation groups as PNG or Markdown. -![dsh-share dialog preview](./assets/readme/share-dialog.webp) +Multi-select Q&As with an interaction consistent with DeepSeek's web app, then download the selection as Markdown. + +![dsh-share multi-turn Q&A selection](./assets/readme/share-selection.en.webp) + +Adjust the image width, font size, and process visibility before downloading or copying the result. + +![dsh-share image generation](./assets/readme/share-dialog.en.webp) ## Features @@ -27,8 +33,6 @@ Add the plugin to the Web Profile with the DSH CLI, then restart `dsh web`: dsh plugin --profile web add dsh-share ``` -To try a prerelease, replace the package name in the installation command with `dsh-share@beta`. A plain `npm install dsh-share` only adds the package to the current Node.js project; it does not enable the DSH plugin. - ## Other installation methods ### From GitHub diff --git a/README.md b/README.md index 47bb63e..7953fc4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,13 @@ DSH 对话分享插件,分享单轮或多轮对话,可导出为图片或 Markdown。 -![dsh-share 分享图片预览](./assets/readme/share-dialog.webp) +和 DeepSeek 网页端一致的多选交互,同时支持将所选问答下载为 Markdown。 + +![dsh-share 多轮问答选择](./assets/readme/share-selection.webp) + +生成图片前可调整宽度、字号和过程显示,完成后可下载或复制图片。 + +![dsh-share 生成图片](./assets/readme/share-dialog.webp) ## 功能 @@ -27,8 +33,6 @@ DSH 对话分享插件,分享单轮或多轮对话,可导出为图片或 Mar dsh plugin --profile web add dsh-share ``` -如需体验预发布版本,将安装命令中的包名替换为 `dsh-share@beta`。普通 `npm install dsh-share` 只会把包加入当前 Node.js 项目,不会启用 DSH 插件。 - ## 其他安装方式 ### 从 GitHub 安装 diff --git a/assets/readme/share-dialog.en.webp b/assets/readme/share-dialog.en.webp new file mode 100644 index 0000000..3674ea2 Binary files /dev/null and b/assets/readme/share-dialog.en.webp differ diff --git a/assets/readme/share-dialog.webp b/assets/readme/share-dialog.webp index 9fcbd0c..28be2a7 100644 Binary files a/assets/readme/share-dialog.webp and b/assets/readme/share-dialog.webp differ diff --git a/assets/readme/share-selection.en.webp b/assets/readme/share-selection.en.webp new file mode 100644 index 0000000..1df3f6a Binary files /dev/null and b/assets/readme/share-selection.en.webp differ diff --git a/assets/readme/share-selection.webp b/assets/readme/share-selection.webp new file mode 100644 index 0000000..e60c68e Binary files /dev/null and b/assets/readme/share-selection.webp differ diff --git a/lib/client.js b/lib/client.js index fc94ee2..a3c6106 100644 --- a/lib/client.js +++ b/lib/client.js @@ -3280,7 +3280,7 @@ window.__ModuleLoader__.load({ //#endregion //#region src/client/index.ts const name = "dsh-share/client"; - const inject = ["slots"]; + const inject = ["slots", "locale"]; const STYLE_ID = "dsh-share-style"; const TRANSPARENT_IMAGE_PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; const STYLE_TEXT = ` @@ -3740,11 +3740,11 @@ window.__ModuleLoader__.load({ overflow: visible !important; } `; - function getLocale(document) { + function getDocumentLocale(document) { return document.documentElement.lang.toLowerCase().startsWith("zh") ? "zh" : "en"; } - function t(document) { - if (getLocale(document) === "zh") return { + function t(locale) { + if (locale === "zh") return { title: "分享当前问答", selectedTitle: () => "生成图片", share: "将当前问答分享为图片", @@ -3857,7 +3857,7 @@ window.__ModuleLoader__.load({ constructor(document, options) { this.document = document; this.options = options; - const strings = t(document); + const strings = t(options.getLocale()); let storage; try { storage = document.defaultView?.localStorage; @@ -3960,8 +3960,39 @@ window.__ModuleLoader__.load({ get settings() { return { ...this.currentSettings }; } + /** 弹窗会跨语言切换复用;每次显示前都从 DSH 官方 locale 刷新静态文案。 */ + updateCopy() { + const strings = t(this.options.getLocale()); + const widthGroup = this.element.querySelector("[data-dsh-share-width]"); + const fontSizeGroup = this.element.querySelector("[data-dsh-share-font-size]"); + const close = this.element.querySelector("[data-dsh-share-close]"); + const labels = { + phone: strings.phone, + tablet: strings.tablet, + desktop: strings.desktop, + normal: strings.normal, + large: strings.large, + xlarge: strings.xlarge + }; + const widthLabel = this.element.querySelector("[data-dsh-share-width-label]"); + const fontSizeLabel = this.element.querySelector("[data-dsh-share-font-size-label]"); + const hideProcessLabel = this.element.querySelector("[data-dsh-share-hide-process-label]"); + if (widthLabel) widthLabel.textContent = strings.width; + if (fontSizeLabel) fontSizeLabel.textContent = strings.fontSize; + if (hideProcessLabel) hideProcessLabel.textContent = strings.hideProcess; + if (widthGroup) widthGroup.ariaLabel = strings.width; + if (fontSizeGroup) fontSizeGroup.ariaLabel = strings.fontSize; + if (close) { + close.title = strings.close; + close.ariaLabel = strings.close; + } + for (const button of this.choiceButtons) button.textContent = labels[button.dataset.value ?? ""] ?? button.textContent; + this.copyButton.textContent = strings.copy; + this.downloadButton.textContent = strings.download; + return strings; + } showLoading(turnCount, preservePreview = false, selectionExport = false) { - const strings = t(this.document); + const strings = this.updateCopy(); const canPreserve = preservePreview && this.blob !== void 0 && this.objectUrl !== void 0; this.title.textContent = selectionExport ? strings.selectedTitle(turnCount) : strings.title; this.element.ariaBusy = "true"; @@ -3983,7 +4014,7 @@ window.__ModuleLoader__.load({ } /** 设置连续变化时先保留当前预览,只更新轻量状态;图片稍后统一重算。 */ showPendingUpdate() { - const strings = t(this.document); + const strings = this.updateCopy(); const canPreserve = this.blob !== void 0 && this.objectUrl !== void 0; this.element.ariaBusy = "true"; this.copyButton.disabled = true; @@ -4032,7 +4063,7 @@ window.__ModuleLoader__.load({ return true; } showError(preservePreview = false) { - const strings = t(this.document); + const strings = t(this.options.getLocale()); const canPreserve = preservePreview && this.blob !== void 0 && this.objectUrl !== void 0; this.element.ariaBusy = "false"; if (canPreserve) { @@ -4123,7 +4154,7 @@ window.__ModuleLoader__.load({ } async copy() { if (!this.blob) return; - const strings = t(this.document); + const strings = t(this.options.getLocale()); const clipboard = this.document.defaultView?.navigator.clipboard; const ClipboardItemConstructor = this.document.defaultView?.ClipboardItem; if (!clipboard?.write || !ClipboardItemConstructor) { @@ -4188,12 +4219,20 @@ window.__ModuleLoader__.load({ button.ariaLabel = wide; button.append(wideLabel, compactLabel); } + function updateResponsiveLabel(button, wide, compact) { + if (button.ariaLabel !== wide) button.ariaLabel = wide; + const wideLabel = button.querySelector("[data-dsh-share-label=\"wide\"]"); + const compactLabel = button.querySelector("[data-dsh-share-label=\"compact\"]"); + if (wideLabel && wideLabel.textContent !== wide) wideLabel.textContent = wide; + if (compactLabel && compactLabel.textContent !== compact) compactLabel.textContent = compact; + } function createShareRuntime(document, options = {}) { const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = STYLE_TEXT; document.head.append(style); const renderImage = options.renderImage ?? renderShareImage; + const currentLocale = options.getLocale ?? (() => getDocumentLocale(document)); let activeContent; let activeGroupCount = 0; let renderEpoch = 0; @@ -4247,35 +4286,50 @@ window.__ModuleLoader__.load({ selections.set(sessionId, controller); return controller; }; - const publishSelection = (controller, active) => { - const total = controller.available.size; - controller.snapshot = { - active, - allSelected: total > 0 && controller.selected.size === total, - count: controller.selected.size, - selectedIds: new Set(controller.selected.keys()), - total - }; + const refreshSelectionCopy = (controller) => { + const strings = t(currentLocale()); if (controller.scroll) for (const button of controller.scroll.querySelectorAll("[data-dsh-share-turn-select]")) { const id = button.dataset.turnId ?? ""; const selected = controller.selected.has(id); button.setAttribute("aria-checked", String(selected)); - button.ariaLabel = selected ? t(document).unselectTurn : t(document).selectTurn; + button.ariaLabel = selected ? t(currentLocale()).unselectTurn : t(currentLocale()).selectTurn; } if (controller.footer) { - const strings = t(document); const all = controller.footer.querySelector("[data-dsh-share-select-all]"); + const allLabel = controller.footer.querySelector("[data-dsh-share-select-all-label]"); const count = controller.footer.querySelector("[data-dsh-share-selection-count]"); + const cancel = controller.footer.querySelector("[data-dsh-share-selection-cancel]"); const markdown = controller.footer.querySelector("[data-dsh-share-selection-markdown]"); const create = controller.footer.querySelector("[data-dsh-share-selection-create]"); - all?.setAttribute("aria-checked", String(controller.snapshot.allSelected)); + if (all) { + all.setAttribute("aria-checked", String(controller.snapshot.allSelected)); + if (all.ariaLabel !== strings.selectAll) all.ariaLabel = strings.selectAll; + } + if (allLabel && allLabel.textContent !== strings.selectAll) allLabel.textContent = strings.selectAll; const nextCount = strings.selectedCount(controller.selected.size); if (count && count.textContent !== nextCount) count.textContent = nextCount; + if (cancel && cancel.textContent !== strings.cancelSelection) cancel.textContent = strings.cancelSelection; + if (markdown) updateResponsiveLabel(markdown, strings.downloadMarkdown, strings.downloadMarkdownCompact); + if (create) updateResponsiveLabel(create, strings.createSelection, strings.createSelectionCompact); if (markdown) markdown.disabled = controller.selected.size === 0; if (create) create.disabled = controller.selected.size === 0; } + }; + const publishSelection = (controller, active) => { + const total = controller.available.size; + controller.snapshot = { + active, + allSelected: total > 0 && controller.selected.size === total, + count: controller.selected.size, + selectedIds: new Set(controller.selected.keys()), + total + }; + refreshSelectionCopy(controller); for (const listener of controller.listeners) listener(); }; + const unsubscribeLocale = options.subscribeLocale?.(() => { + for (const controller of selections.values()) if (controller.snapshot.active) refreshSelectionCopy(controller); + }); const cleanupSelectionDom = (controller) => { const scroll = controller.scroll; const scrollTop = scroll?.scrollTop; @@ -4429,7 +4483,7 @@ window.__ModuleLoader__.load({ }); }; const createSelectionFooter = (controller) => { - const strings = t(document); + const strings = t(currentLocale()); const footer = document.createElement("div"); footer.dataset.dshShareSelectionFooter = ""; const inner = document.createElement("div"); @@ -4440,6 +4494,7 @@ window.__ModuleLoader__.load({ const selectAllBox = document.createElement("span"); selectAllBox.dataset.dshShareSelectAllBox = ""; const selectAllLabel = document.createElement("span"); + selectAllLabel.dataset.dshShareSelectAllLabel = ""; selectAllLabel.textContent = strings.selectAll; selectAll.append(selectAllBox, selectAllLabel); selectAll.addEventListener("click", () => toggleAll(controller)); @@ -4455,7 +4510,7 @@ window.__ModuleLoader__.load({ markdown.addEventListener("click", () => { const messages = selectedTurnsToShareMessages(controller.selected.values()); if (messages.length === 0) return; - downloadMarkdownFile(document, createShareMarkdown(messages, getLocale(document), dialog.settings)); + downloadMarkdownFile(document, createShareMarkdown(messages, currentLocale(), dialog.settings)); }); const create = makeButton(document, "dshShareSelectionCreate"); appendResponsiveLabel(document, create, strings.createSelection, strings.createSelectionCompact); @@ -4476,7 +4531,7 @@ window.__ModuleLoader__.load({ settingsRenderTimer = void 0; }; const createRenderRequest = (content, groupCount, preservePreview = false, epoch = ++renderEpoch) => { - const locale = getLocale(document); + const locale = currentLocale(); const settings = dialog.settings; dialog.showLoading(groupCount, preservePreview, true); return { @@ -4545,6 +4600,7 @@ window.__ModuleLoader__.load({ renderEpoch += 1; }; dialog = new PreviewDialog(document, { + getLocale: currentLocale, onSettingsChange: () => { if (activeContent) scheduleSettingsRender(activeContent, activeGroupCount); }, @@ -4605,6 +4661,7 @@ window.__ModuleLoader__.load({ let disposed = false; return { document, + getLocale: currentLocale, selectionFor, enterSelection, cancelSelection: (sessionId) => { @@ -4633,14 +4690,15 @@ window.__ModuleLoader__.load({ controller.snapshots.clear(); } selections.clear(); + unsubscribeLocale?.(); style.remove(); dialog.destroy(); } }; } /** 官方 assistant-actions 插槽中的分享入口。 */ - function ShareAction({ messageId, sessionId, shareRuntime, useSession, useShareSelection }) { - const strings = t(shareRuntime.document); + function ShareAction({ messageId, sessionId, shareRuntime, useSession, useShareLocale, useShareSelection }) { + const strings = t(useShareLocale((snapshot) => snapshot.active)); const selection = useShareSelection((snapshot) => snapshot); const turn = useSession((snapshot) => { for (const node of snapshot.chat.nodes.values()) { @@ -4666,8 +4724,8 @@ window.__ModuleLoader__.load({ }); } /** 官方 Session Header 右侧 utilities 插槽中的多轮分享入口。 */ - function ShareConversationAction({ sessionId, shareRuntime, useShareSelection }) { - const strings = t(shareRuntime.document); + function ShareConversationAction({ sessionId, shareRuntime, useShareLocale, useShareSelection }) { + const strings = t(useShareLocale((snapshot) => snapshot.active)); if (useShareSelection((snapshot) => snapshot).active) return (0, react.createElement)(react.Fragment); const button = (0, react.createElement)("button", { type: "button", @@ -4687,13 +4745,19 @@ window.__ModuleLoader__.load({ let sharedRuntime; let registrations = 0; const runtimeForRegistration = () => { - sharedRuntime ??= createShareRuntime(document); + sharedRuntime ??= createShareRuntime(document, { + getLocale: () => ctx.locale.getLocale().active, + subscribeLocale: (listener) => ctx.locale.subscribe(listener) + }); return sharedRuntime; }; const injectFace = (sessionId) => { const runtime = runtimeForRegistration(); return { - hooks: { shareSelection: runtime.selectionFor(String(sessionId)) }, + hooks: { + shareLocale: ctx.locale, + shareSelection: runtime.selectionFor(String(sessionId)) + }, shareRuntime: runtime }; }; diff --git a/lib/types/client/index.d.ts b/lib/types/client/index.d.ts index 4657d96..b83b435 100644 --- a/lib/types/client/index.d.ts +++ b/lib/types/client/index.d.ts @@ -1,11 +1,15 @@ import type { ClientContext, ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client'; +import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'; import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'; import { type ReactElement } from 'react'; export declare const name = "dsh-share/client"; export declare const inject: string[]; export type ImageRenderer = (element: HTMLElement) => Promise; +type ShareLocale = 'zh' | 'en'; export interface InstallOptions { + getLocale?: () => ShareLocale; renderImage?: ImageRenderer; + subscribeLocale?: (listener: () => void) => () => void; } export declare function renderShareImage(element: HTMLElement): Promise; export interface ShareSelectionSnapshot { @@ -17,6 +21,7 @@ export interface ShareSelectionSnapshot { } export interface ShareRuntime { readonly document: Document; + getLocale(): ShareLocale; selectionFor(sessionId: string): ObservableSnapshot; enterSelection(sessionId: string, source?: HTMLElement, initialTurn?: number): void; cancelSelection(sessionId: string): void; @@ -27,6 +32,7 @@ export interface ShareRuntime { export declare function createShareRuntime(document: Document, options?: InstallOptions): ShareRuntime; interface ShareRuntimeInjected { hooks: { + shareLocale: ObservableSnapshot; shareSelection: ObservableSnapshot; }; shareRuntime: ShareRuntime; @@ -34,9 +40,9 @@ interface ShareRuntimeInjected { export type ShareActionProps = PropsRuntime<'conversation.chat.assistant-actions'> & InjectFace; export type ShareConversationActionProps = PropsRuntime<'conversation.session.header.utilities'> & InjectFace; /** 官方 assistant-actions 插槽中的分享入口。 */ -export declare function ShareAction({ messageId, sessionId, shareRuntime, useSession, useShareSelection, }: ShareActionProps): ReactElement; +export declare function ShareAction({ messageId, sessionId, shareRuntime, useSession, useShareLocale, useShareSelection, }: ShareActionProps): ReactElement; /** 官方 Session Header 右侧 utilities 插槽中的多轮分享入口。 */ -export declare function ShareConversationAction({ sessionId, shareRuntime, useShareSelection, }: ShareConversationActionProps): ReactElement; +export declare function ShareConversationAction({ sessionId, shareRuntime, useShareLocale, useShareSelection, }: ShareConversationActionProps): ReactElement; export declare function apply(ctx: ClientContext): void; export { createShareCard } from './card.ts'; export { createShareMarkdown } from './markdown.ts'; diff --git a/lib/types/client/index.d.ts.map b/lib/types/client/index.d.ts.map index b788182..0701085 100644 --- a/lib/types/client/index.d.ts.map +++ b/lib/types/client/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAA;AAG/F,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAChF,OAAO,EAA+D,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAsBtG,eAAO,MAAM,IAAI,qBAAqB,CAAA;AACtC,eAAO,MAAM,MAAM,UAAY,CAAA;AAgd/B,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEnE,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,aAAa,CAAA;CAC5B;AA6HD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAgXD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;CACd;AA0BD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAAC,sBAAsB,CAAC,CAAA;IAC3E,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnF,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,OAAO,IAAI,IAAI,CAAA;CAChB;AA0DD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CAuhBjG;AAED,UAAU,oBAAoB;IAC5B,KAAK,EAAE;QAAE,cAAc,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAA;KAAE,CAAA;IACrE,YAAY,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,MAAM,gBAAgB,GAC1B,YAAY,CAAC,qCAAqC,CAAC,GACjD,UAAU,CAAC,oBAAoB,CAAC,CAAA;AAEpC,MAAM,MAAM,4BAA4B,GACtC,YAAY,CAAC,uCAAuC,CAAC,GACnD,UAAU,CAAC,oBAAoB,CAAC,CAAA;AAEpC,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,GAClE,EAAE,gBAAgB,GAAG,YAAY,CA6BjC;AAED,iDAAiD;AACjD,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EAAE,YAAY,EAAE,iBAAiB,GAC3C,EAAE,4BAA4B,GAAG,YAAY,CAa7C;AAED,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA2C9C;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AACrE,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,eAAe,CAAA"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAA;AAC/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AAG3E,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAChF,OAAO,EAA+D,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAsBtG,eAAO,MAAM,IAAI,qBAAqB,CAAA;AACtC,eAAO,MAAM,MAAM,UAAsB,CAAA;AAgdzC,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AACnE,KAAK,WAAW,GAAG,IAAI,GAAG,IAAI,CAAA;AAE9B,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,WAAW,CAAA;IAC7B,WAAW,CAAC,EAAE,aAAa,CAAA;IAC3B,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CACvD;AA6HD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAmZD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;CACd;AA0BD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,SAAS,IAAI,WAAW,CAAA;IACxB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAAC,sBAAsB,CAAC,CAAA;IAC3E,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnF,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACxD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,OAAO,IAAI,IAAI,CAAA;CAChB;AAsED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CAmjBjG;AAED,UAAU,oBAAoB;IAC5B,KAAK,EAAE;QACL,WAAW,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAA;QAC/C,cAAc,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAA;KAC3D,CAAA;IACD,YAAY,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,MAAM,gBAAgB,GAC1B,YAAY,CAAC,qCAAqC,CAAC,GACjD,UAAU,CAAC,oBAAoB,CAAC,CAAA;AAEpC,MAAM,MAAM,4BAA4B,GACtC,YAAY,CAAC,uCAAuC,CAAC,GACnD,UAAU,CAAC,oBAAoB,CAAC,CAAA;AAEpC,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,GAClF,EAAE,gBAAgB,GAAG,YAAY,CA6BjC;AAED,iDAAiD;AACjD,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,GAC3D,EAAE,4BAA4B,GAAG,YAAY,CAa7C;AAED,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiD9C;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAA;AACrE,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,eAAe,CAAA"} \ No newline at end of file diff --git a/package.json b/package.json index 96b79d8..3b52fc8 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ }, "client": { "inject": [ + "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-ui-primitives" @@ -65,12 +66,14 @@ } }, "peerDependencies": { + "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6", "react": "^18.2.0" }, "devDependencies": { + "@deepseek-ai/dsh-client-locale": "0.1.0-rc.6", "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a5468b..0ba1a72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@deepseek-ai/dsh-client-locale': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(9e8db6c5abfb5dfda585a92ec8fe7fec) '@deepseek-ai/dsh-client-runtime': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(5ee287bd07c00b3171c7ee342e9431a4) diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 4a8a1db..1fee089 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -25,7 +25,10 @@ const REQUIRED_FILES = [ 'CHANGELOG.md', 'LICENSE', 'THIRD_PARTY_LICENSES.md', + 'assets/readme/share-selection.webp', + 'assets/readme/share-selection.en.webp', 'assets/readme/share-dialog.webp', + 'assets/readme/share-dialog.en.webp', ] const FORBIDDEN_PREFIXES = ['src/', 'tests/', 'scripts/', '.github/', '.docs/'] diff --git a/src/client/index.ts b/src/client/index.ts index 2109c7b..8924a9b 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,5 +1,6 @@ import { toBlob } from 'html-to-image' import type { ClientContext, ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client' +import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { IconShareOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' @@ -26,7 +27,7 @@ import { } from './settings.ts' export const name = 'dsh-share/client' -export const inject = ['slots'] +export const inject = ['slots', 'locale'] const STYLE_ID = 'dsh-share-style' const TRANSPARENT_IMAGE_PLACEHOLDER = @@ -491,9 +492,12 @@ const STYLE_TEXT = ` ` export type ImageRenderer = (element: HTMLElement) => Promise +type ShareLocale = 'zh' | 'en' export interface InstallOptions { + getLocale?: () => ShareLocale renderImage?: ImageRenderer + subscribeLocale?: (listener: () => void) => () => void } interface RenderRequest { @@ -540,12 +544,12 @@ interface Translation { xlarge: string } -function getLocale(document: Document): 'zh' | 'en' { +function getDocumentLocale(document: Document): ShareLocale { return document.documentElement.lang.toLowerCase().startsWith('zh') ? 'zh' : 'en' } -function t(document: Document): Translation { - if (getLocale(document) === 'zh') { +function t(locale: ShareLocale): Translation { + if (locale === 'zh') { return { title: '分享当前问答', selectedTitle: () => '生成图片', @@ -646,6 +650,7 @@ function downloadMarkdownFile(document: Document, markdown: string): void { } interface PreviewDialogOptions { + getLocale(): ShareLocale onSettingsChange(settings: ShareSettings): void onDismiss(): void } @@ -669,7 +674,7 @@ class PreviewDialog { private readonly document: Document, private readonly options: PreviewDialogOptions, ) { - const strings = t(document) + const strings = t(options.getLocale()) let storage: Storage | undefined try { storage = document.defaultView?.localStorage @@ -780,12 +785,46 @@ class PreviewDialog { return { ...this.currentSettings } } + /** 弹窗会跨语言切换复用;每次显示前都从 DSH 官方 locale 刷新静态文案。 */ + private updateCopy(): Translation { + const strings = t(this.options.getLocale()) + const widthGroup = this.element.querySelector('[data-dsh-share-width]') + const fontSizeGroup = this.element.querySelector('[data-dsh-share-font-size]') + const close = this.element.querySelector('[data-dsh-share-close]') + const labels: Record = { + phone: strings.phone, + tablet: strings.tablet, + desktop: strings.desktop, + normal: strings.normal, + large: strings.large, + xlarge: strings.xlarge, + } + const widthLabel = this.element.querySelector('[data-dsh-share-width-label]') + const fontSizeLabel = this.element.querySelector('[data-dsh-share-font-size-label]') + const hideProcessLabel = this.element.querySelector('[data-dsh-share-hide-process-label]') + if (widthLabel) widthLabel.textContent = strings.width + if (fontSizeLabel) fontSizeLabel.textContent = strings.fontSize + if (hideProcessLabel) hideProcessLabel.textContent = strings.hideProcess + if (widthGroup) widthGroup.ariaLabel = strings.width + if (fontSizeGroup) fontSizeGroup.ariaLabel = strings.fontSize + if (close) { + close.title = strings.close + close.ariaLabel = strings.close + } + for (const button of this.choiceButtons) { + button.textContent = labels[button.dataset.value ?? ''] ?? button.textContent + } + this.copyButton.textContent = strings.copy + this.downloadButton.textContent = strings.download + return strings + } + showLoading( turnCount: number, preservePreview = false, selectionExport = false, ): void { - const strings = t(this.document) + const strings = this.updateCopy() const canPreserve = preservePreview && this.blob !== undefined && this.objectUrl !== undefined this.title.textContent = selectionExport ? strings.selectedTitle(turnCount) : strings.title this.element.ariaBusy = 'true' @@ -810,7 +849,7 @@ class PreviewDialog { /** 设置连续变化时先保留当前预览,只更新轻量状态;图片稍后统一重算。 */ showPendingUpdate(): void { - const strings = t(this.document) + const strings = this.updateCopy() const canPreserve = this.blob !== undefined && this.objectUrl !== undefined this.element.ariaBusy = 'true' this.copyButton.disabled = true @@ -865,7 +904,7 @@ class PreviewDialog { } showError(preservePreview = false): void { - const strings = t(this.document) + const strings = t(this.options.getLocale()) const canPreserve = preservePreview && this.blob !== undefined && this.objectUrl !== undefined this.element.ariaBusy = 'false' if (canPreserve) { @@ -969,7 +1008,7 @@ class PreviewDialog { private async copy(): Promise { if (!this.blob) return - const strings = t(this.document) + const strings = t(this.options.getLocale()) const clipboard = this.document.defaultView?.navigator.clipboard const ClipboardItemConstructor = this.document.defaultView?.ClipboardItem if (!clipboard?.write || !ClipboardItemConstructor) { @@ -1030,6 +1069,7 @@ interface SessionSelection extends ObservableSnapshot { export interface ShareRuntime { readonly document: Document + getLocale(): ShareLocale selectionFor(sessionId: string): ObservableSnapshot enterSelection(sessionId: string, source?: HTMLElement, initialTurn?: number): void cancelSelection(sessionId: string): void @@ -1094,6 +1134,18 @@ function appendResponsiveLabel( button.append(wideLabel, compactLabel) } +function updateResponsiveLabel( + button: HTMLButtonElement, + wide: string, + compact: string, +): void { + if (button.ariaLabel !== wide) button.ariaLabel = wide + const wideLabel = button.querySelector('[data-dsh-share-label="wide"]') + const compactLabel = button.querySelector('[data-dsh-share-label="compact"]') + if (wideLabel && wideLabel.textContent !== wide) wideLabel.textContent = wide + if (compactLabel && compactLabel.textContent !== compact) compactLabel.textContent = compact +} + export function createShareRuntime(document: Document, options: InstallOptions = {}): ShareRuntime { const style = document.createElement('style') style.id = STYLE_ID @@ -1101,6 +1153,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = document.head.append(style) const renderImage = options.renderImage ?? renderShareImage + const currentLocale = options.getLocale ?? (() => getDocumentLocale(document)) let activeContent: readonly ShareMessage[] | undefined let activeGroupCount = 0 let renderEpoch = 0 @@ -1152,15 +1205,8 @@ export function createShareRuntime(document: Document, options: InstallOptions = return controller } - const publishSelection = (controller: SessionSelection, active: boolean): void => { - const total = controller.available.size - controller.snapshot = { - active, - allSelected: total > 0 && controller.selected.size === total, - count: controller.selected.size, - selectedIds: new Set(controller.selected.keys()), - total, - } + const refreshSelectionCopy = (controller: SessionSelection): void => { + const strings = t(currentLocale()) if (controller.scroll) { for (const button of controller.scroll.querySelectorAll( '[data-dsh-share-turn-select]', @@ -1168,25 +1214,55 @@ export function createShareRuntime(document: Document, options: InstallOptions = const id = button.dataset.turnId ?? '' const selected = controller.selected.has(id) button.setAttribute('aria-checked', String(selected)) - button.ariaLabel = selected ? t(document).unselectTurn : t(document).selectTurn + button.ariaLabel = selected ? t(currentLocale()).unselectTurn : t(currentLocale()).selectTurn } } if (controller.footer) { - const strings = t(document) const all = controller.footer.querySelector('[data-dsh-share-select-all]') + const allLabel = controller.footer.querySelector('[data-dsh-share-select-all-label]') const count = controller.footer.querySelector('[data-dsh-share-selection-count]') + const cancel = controller.footer.querySelector('[data-dsh-share-selection-cancel]') const markdown = controller.footer.querySelector('[data-dsh-share-selection-markdown]') const create = controller.footer.querySelector('[data-dsh-share-selection-create]') - all?.setAttribute('aria-checked', String(controller.snapshot.allSelected)) + if (all) { + all.setAttribute('aria-checked', String(controller.snapshot.allSelected)) + if (all.ariaLabel !== strings.selectAll) all.ariaLabel = strings.selectAll + } + if (allLabel && allLabel.textContent !== strings.selectAll) allLabel.textContent = strings.selectAll const nextCount = strings.selectedCount(controller.selected.size) // footer 位于被观察的会话区域内;仅在文案变化时写入,避免自身触发 MutationObserver 循环。 if (count && count.textContent !== nextCount) count.textContent = nextCount + if (cancel && cancel.textContent !== strings.cancelSelection) cancel.textContent = strings.cancelSelection + if (markdown) { + updateResponsiveLabel(markdown, strings.downloadMarkdown, strings.downloadMarkdownCompact) + } + if (create) { + updateResponsiveLabel(create, strings.createSelection, strings.createSelectionCompact) + } if (markdown) markdown.disabled = controller.selected.size === 0 if (create) create.disabled = controller.selected.size === 0 } + } + + const publishSelection = (controller: SessionSelection, active: boolean): void => { + const total = controller.available.size + controller.snapshot = { + active, + allSelected: total > 0 && controller.selected.size === total, + count: controller.selected.size, + selectedIds: new Set(controller.selected.keys()), + total, + } + refreshSelectionCopy(controller) for (const listener of controller.listeners) listener() } + const unsubscribeLocale = options.subscribeLocale?.(() => { + for (const controller of selections.values()) { + if (controller.snapshot.active) refreshSelectionCopy(controller) + } + }) + const cleanupSelectionDom = (controller: SessionSelection): void => { const scroll = controller.scroll const scrollTop = scroll?.scrollTop @@ -1385,7 +1461,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = } const createSelectionFooter = (controller: SessionSelection): HTMLElement => { - const strings = t(document) + const strings = t(currentLocale()) const footer = document.createElement('div') footer.dataset.dshShareSelectionFooter = '' const inner = document.createElement('div') @@ -1397,6 +1473,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = const selectAllBox = document.createElement('span') selectAllBox.dataset.dshShareSelectAllBox = '' const selectAllLabel = document.createElement('span') + selectAllLabel.dataset.dshShareSelectAllLabel = '' selectAllLabel.textContent = strings.selectAll selectAll.append(selectAllBox, selectAllLabel) selectAll.addEventListener('click', () => toggleAll(controller)) @@ -1420,7 +1497,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = markdown.addEventListener('click', () => { const messages = selectedTurnsToShareMessages(controller.selected.values()) if (messages.length === 0) return - downloadMarkdownFile(document, createShareMarkdown(messages, getLocale(document), dialog.settings)) + downloadMarkdownFile(document, createShareMarkdown(messages, currentLocale(), dialog.settings)) }) const create = makeButton(document, 'dshShareSelectionCreate') @@ -1455,7 +1532,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = preservePreview = false, epoch = ++renderEpoch, ): RenderRequest => { - const locale = getLocale(document) + const locale = currentLocale() const settings = dialog.settings dialog.showLoading(groupCount, preservePreview, true) return { content, epoch, locale, preservePreview, settings } @@ -1534,6 +1611,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = } dialog = new PreviewDialog(document, { + getLocale: currentLocale, onSettingsChange: () => { if (activeContent) scheduleSettingsRender(activeContent, activeGroupCount) }, @@ -1597,6 +1675,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = let disposed = false return { document, + getLocale: currentLocale, selectionFor, enterSelection, cancelSelection: (sessionId) => { @@ -1625,6 +1704,7 @@ export function createShareRuntime(document: Document, options: InstallOptions = controller.snapshots.clear() } selections.clear() + unsubscribeLocale?.() style.remove() dialog.destroy() }, @@ -1632,7 +1712,10 @@ export function createShareRuntime(document: Document, options: InstallOptions = } interface ShareRuntimeInjected { - hooks: { shareSelection: ObservableSnapshot } + hooks: { + shareLocale: ObservableSnapshot + shareSelection: ObservableSnapshot + } shareRuntime: ShareRuntime } @@ -1646,9 +1729,9 @@ export type ShareConversationActionProps = /** 官方 assistant-actions 插槽中的分享入口。 */ export function ShareAction({ - messageId, sessionId, shareRuntime, useSession, useShareSelection, + messageId, sessionId, shareRuntime, useSession, useShareLocale, useShareSelection, }: ShareActionProps): ReactElement { - const strings = t(shareRuntime.document) + const strings = t(useShareLocale(snapshot => snapshot.active)) const selection = useShareSelection(snapshot => snapshot) const turn = useSession((snapshot) => { for (const node of snapshot.chat.nodes.values()) { @@ -1680,9 +1763,9 @@ export function ShareAction({ /** 官方 Session Header 右侧 utilities 插槽中的多轮分享入口。 */ export function ShareConversationAction({ - sessionId, shareRuntime, useShareSelection, + sessionId, shareRuntime, useShareLocale, useShareSelection, }: ShareConversationActionProps): ReactElement { - const strings = t(shareRuntime.document) + const strings = t(useShareLocale(snapshot => snapshot.active)) const selection = useShareSelection(snapshot => snapshot) if (selection.active) return createElement(Fragment) const button = createElement('button', { @@ -1700,13 +1783,19 @@ export function apply(ctx: ClientContext): void { let sharedRuntime: ShareRuntime | undefined let registrations = 0 const runtimeForRegistration = (): ShareRuntime => { - sharedRuntime ??= createShareRuntime(document) + sharedRuntime ??= createShareRuntime(document, { + getLocale: () => ctx.locale.getLocale().active, + subscribeLocale: listener => ctx.locale.subscribe(listener), + }) return sharedRuntime } const injectFace = (sessionId: unknown): ShareRuntimeInjected => { const runtime = runtimeForRegistration() return { - hooks: { shareSelection: runtime.selectionFor(String(sessionId)) }, + hooks: { + shareLocale: ctx.locale, + shareSelection: runtime.selectionFor(String(sessionId)), + }, shareRuntime: runtime, } } diff --git a/tests/client.spec.ts b/tests/client.spec.ts index 86f0ca8..884285b 100644 --- a/tests/client.spec.ts +++ b/tests/client.spec.ts @@ -79,10 +79,16 @@ function actionProps( sessionId = 'session-1', ): ShareActionProps { const selection = runtime.selectionFor(sessionId) + const locale = { + active: runtime.getLocale(), + locales: [], + revision: 0, + } as const return { messageId, sessionId, shareRuntime: runtime, + useShareLocale: selector => selector(locale), useShareSelection: selector => selector(selection.getSnapshot()), useSession: selector => selector({ chat: { @@ -99,9 +105,15 @@ function actionProps( function headerProps(runtime: ShareRuntime, sessionId = 'session-1'): ShareConversationActionProps { const selection = runtime.selectionFor(sessionId) + const locale = { + active: runtime.getLocale(), + locales: [], + revision: 0, + } as const return { sessionId, shareRuntime: runtime, + useShareLocale: selector => selector(locale), useShareSelection: selector => selector(selection.getSnapshot()), } as ShareConversationActionProps } @@ -150,14 +162,24 @@ describe('分享按钮运行时', () => { const fixture = createConversation() addTurn(fixture, 'one', 1) const disposeRegistration = vi.fn() + const disposeLocale = vi.fn() const injectionDisposers: Array<() => void> = [] const register = vi.fn(() => disposeRegistration) + const subscribeLocale = vi.fn(() => disposeLocale) const injectSlot = vi.fn((_name: string, callback: () => void | (() => void)) => { const dispose = callback() if (dispose) injectionDisposers.push(dispose) }) - apply({ slots: { inject: injectSlot, register } } as never) + const localeSnapshot = { active: 'zh', locales: [], revision: 0 } as const + apply({ + locale: { + getLocale: () => localeSnapshot, + getSnapshot: () => localeSnapshot, + subscribe: subscribeLocale, + }, + slots: { inject: injectSlot, register }, + } as never) expect(injectSlot).toHaveBeenCalledWith('conversation.chat.assistant-actions', expect.any(Function)) expect(injectSlot).toHaveBeenCalledWith('conversation.session.header.utilities', expect.any(Function)) @@ -196,6 +218,7 @@ describe('分享按钮运行时', () => { order: -10, }) expect(headerOptions.inject('session-one').shareRuntime).toBe(runtime) + expect(subscribeLocale).toHaveBeenCalledOnce() const action = actionComponent(actionProps(runtime, 'message-one')) expect(action.type).toBe(Tooltip) @@ -229,21 +252,73 @@ describe('分享按钮运行时', () => { for (const dispose of injectionDisposers) dispose() expect(disposeRegistration).toHaveBeenCalledTimes(2) + expect(disposeLocale).toHaveBeenCalledOnce() expect(document.querySelector('[data-dsh-share-dialog]')).toBeNull() expect(document.getElementById('dsh-share-style')).toBeNull() }) - it('英文界面的两个 Tooltip 使用英文文案', () => { - document.documentElement.lang = 'en' - const runtime = createShareRuntime(document) + it('使用 DSH 官方语言状态渲染英文,而不是依赖固定的 HTML lang', () => { + const fixture = createConversation() + const tail = addTurn(fixture, 'one', 1) + let locale: 'zh' | 'en' = 'en' + const runtime = createShareRuntime(document, { + getLocale: () => locale, + renderImage: vi.fn(async () => new Blob(['image'], { type: 'image/png' })), + }) const action = ShareAction(actionProps(runtime, 'message-en')) const header = ShareConversationAction(headerProps(runtime)) expect(action.props.label).toBe('Share') expect(action.props.children.props['aria-label']).toBe('Share this Q&A as an image') expect(header.props.label).toBe('Share conversation') + expect(document.querySelector('[data-dsh-share-close]')?.getAttribute('aria-label')).toBe('Close') + + locale = 'zh' + triggerShareAction(tail, runtime) + document.querySelector('[data-dsh-share-selection-create]')?.click() + expect(document.querySelector('[data-dsh-share-close]')?.getAttribute('aria-label')).toBe('关闭') + expect(document.querySelector('[data-dsh-share-choice][data-value="phone"]')?.textContent).toBe('手机') + + runtime.dispose() + }) + + it('选择模式打开时跟随 DSH 语言刷新完整操作栏', () => { + const fixture = createConversation() + addTurn(fixture, 'one', 1) + let locale: 'zh' | 'en' = 'zh' + let notifyLocale = (): void => undefined + const disposeLocale = vi.fn() + const runtime = createShareRuntime(document, { + getLocale: () => locale, + subscribeLocale: (listener) => { + notifyLocale = listener + return disposeLocale + }, + }) + clickHeaderShare(runtime, fixture.source) + + locale = 'en' + notifyLocale() + + const footer = fixture.scroll.querySelector('[data-dsh-share-selection-footer]') as HTMLElement + const selectAll = footer.querySelector('[data-dsh-share-select-all]') + const cancel = footer.querySelector('[data-dsh-share-selection-cancel]') + const markdown = footer.querySelector('[data-dsh-share-selection-markdown]') + const create = footer.querySelector('[data-dsh-share-selection-create]') + expect(selectAll?.ariaLabel).toBe('Select all') + expect(selectAll?.textContent).toBe('Select all') + expect(footer.querySelector('[data-dsh-share-selection-count]')?.textContent) + .toBe('1 conversation group selected') + expect(cancel?.textContent).toBe('Cancel') + expect(markdown?.ariaLabel).toBe('Download Markdown') + expect(markdown?.querySelector('[data-dsh-share-label="compact"]')?.textContent).toBe('Markdown') + expect(create?.ariaLabel).toBe('Create image') + expect(create?.querySelector('[data-dsh-share-label="wide"]')?.textContent).toBe('Create image') + expect([...fixture.scroll.querySelectorAll('[data-dsh-share-turn-select]')] + .every(button => button.ariaLabel === 'Unselect this conversation group')).toBe(true) runtime.dispose() + expect(disposeLocale).toHaveBeenCalledOnce() }) it('运行时重复释放不会报错', () => { diff --git a/tests/package.spec.ts b/tests/package.spec.ts index 573bb4c..999d530 100644 --- a/tests/package.spec.ts +++ b/tests/package.spec.ts @@ -42,6 +42,7 @@ describe('DSH 插件清单', () => { expect(packageJson.dsh.bundle.patch).toBe('./cordis.patch.yml') expect(packageJson.dsh.client.platform).toBe('web') expect(packageJson.dsh.client.inject).toEqual([ + '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-primitives', @@ -94,7 +95,6 @@ describe('DSH 插件清单', () => { expect(chineseReadme).toContain('[English](./README.en.md)') expect(chineseReadme).toContain('dsh plugin --profile web add dsh-share') - expect(chineseReadme).toContain('dsh-share@beta') expect(chineseReadme).toContain('conversation.chat.assistant-actions') expect(chineseReadme).toContain('conversation.session.header.utilities') expect(chineseReadme).toContain('下载 PNG 或 Markdown') @@ -103,8 +103,11 @@ describe('DSH 插件清单', () => { expect(chineseReadme).toContain('不依赖 CSS Module 生成的类名') expect(chineseReadme).toContain('[dshfind.com](https://dshfind.com) DSH 插件超市') expect(englishReadme).toContain('[简体中文](./README.md)') + expect(chineseReadme).toContain('./assets/readme/share-selection.webp') + expect(chineseReadme).toContain('./assets/readme/share-dialog.webp') + expect(englishReadme).toContain('./assets/readme/share-selection.en.webp') + expect(englishReadme).toContain('./assets/readme/share-dialog.en.webp') expect(englishReadme).toContain('dsh plugin --profile web add dsh-share') - expect(englishReadme).toContain('dsh-share@beta') expect(englishReadme).toContain('conversation.chat.assistant-actions') expect(englishReadme).toContain('conversation.session.header.utilities') expect(englishReadme).toContain('PNG download, and Markdown download') diff --git a/tests/release.spec.ts b/tests/release.spec.ts index 4e65607..62fb298 100644 --- a/tests/release.spec.ts +++ b/tests/release.spec.ts @@ -42,7 +42,8 @@ function packReport(overrides: Record = {}) { 'package.json', 'lib/index.js', 'lib/client.js', 'lib/types/index.d.ts', 'lib/types/client/index.d.ts', 'cordis.patch.yml', 'README.md', 'README.en.md', 'CHANGELOG.md', 'LICENSE', 'THIRD_PARTY_LICENSES.md', - 'assets/readme/share-dialog.webp', + 'assets/readme/share-selection.webp', 'assets/readme/share-selection.en.webp', + 'assets/readme/share-dialog.webp', 'assets/readme/share-dialog.en.webp', ].map(path => ({ mode: 0o644, path, size: 1 })) return JSON.stringify([{ filename: `dsh-share-${FIXTURE_VERSION}.tgz`,