Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 功能

Expand All @@ -27,8 +33,6 @@ DSH 对话分享插件,分享单轮或多轮对话,可导出为图片或 Mar
dsh plugin --profile web add dsh-share
```

如需体验预发布版本,将安装命令中的包名替换为 `dsh-share@beta`。普通 `npm install dsh-share` 只会把包加入当前 Node.js 项目,不会启用 DSH 插件。

## 其他安装方式

### 从 GitHub 安装
Expand Down
Binary file added assets/readme/share-dialog.en.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme/share-dialog.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/share-selection.en.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/readme/share-selection.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 94 additions & 30 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down Expand Up @@ -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: "将当前问答分享为图片",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -4545,6 +4600,7 @@ window.__ModuleLoader__.load({
renderEpoch += 1;
};
dialog = new PreviewDialog(document, {
getLocale: currentLocale,
onSettingsChange: () => {
if (activeContent) scheduleSettingsRender(activeContent, activeGroupCount);
},
Expand Down Expand Up @@ -4605,6 +4661,7 @@ window.__ModuleLoader__.load({
let disposed = false;
return {
document,
getLocale: currentLocale,
selectionFor,
enterSelection,
cancelSelection: (sessionId) => {
Expand Down Expand Up @@ -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()) {
Expand All @@ -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",
Expand All @@ -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
};
};
Expand Down
10 changes: 8 additions & 2 deletions lib/types/client/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<Blob>;
type ShareLocale = 'zh' | 'en';
export interface InstallOptions {
getLocale?: () => ShareLocale;
renderImage?: ImageRenderer;
subscribeLocale?: (listener: () => void) => () => void;
}
export declare function renderShareImage(element: HTMLElement): Promise<Blob>;
export interface ShareSelectionSnapshot {
Expand All @@ -17,6 +21,7 @@ export interface ShareSelectionSnapshot {
}
export interface ShareRuntime {
readonly document: Document;
getLocale(): ShareLocale;
selectionFor(sessionId: string): ObservableSnapshot<ShareSelectionSnapshot>;
enterSelection(sessionId: string, source?: HTMLElement, initialTurn?: number): void;
cancelSelection(sessionId: string): void;
Expand All @@ -27,16 +32,17 @@ export interface ShareRuntime {
export declare function createShareRuntime(document: Document, options?: InstallOptions): ShareRuntime;
interface ShareRuntimeInjected {
hooks: {
shareLocale: ObservableSnapshot<LocaleSnapshot>;
shareSelection: ObservableSnapshot<ShareSelectionSnapshot>;
};
shareRuntime: ShareRuntime;
}
export type ShareActionProps = PropsRuntime<'conversation.chat.assistant-actions'> & InjectFace<ShareRuntimeInjected>;
export type ShareConversationActionProps = PropsRuntime<'conversation.session.header.utilities'> & InjectFace<ShareRuntimeInjected>;
/** 官方 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';
Expand Down
Loading
Loading