dsh-image-gen 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.en.md +148 -133
  2. package/README.md +148 -133
  3. package/docs/assets/readme/canvas-edit.webp +0 -0
  4. package/docs/assets/readme/canvas-sketch.webp +0 -0
  5. package/docs/assets/readme/chat-batch.webp +0 -0
  6. package/docs/assets/readme/config.webp +0 -0
  7. package/docs/assets/readme/hero-batch.webp +0 -0
  8. package/docs/assets/readme/hero-canvas.webp +0 -0
  9. package/docs/assets/readme/studio-ref.webp +0 -0
  10. package/docs/assets/readme/studio.webp +0 -0
  11. package/lib/client.js +10848 -6335
  12. package/lib/index.js +2625 -145
  13. package/lib/types/capabilities.d.ts +10 -6
  14. package/lib/types/client/browser-image-utils.d.ts +6 -0
  15. package/lib/types/client/conversation-image-revisions.d.ts +3 -0
  16. package/lib/types/client/conversation-regenerate.d.ts +9 -1
  17. package/lib/types/client/datalist-scroll-fix.d.ts +32 -0
  18. package/lib/types/client/gallery-batch-bar.d.ts +8 -0
  19. package/lib/types/client/gallery-card.d.ts +21 -0
  20. package/lib/types/client/gallery-format.d.ts +7 -0
  21. package/lib/types/client/gallery-i18n.d.ts +236 -0
  22. package/lib/types/client/gallery-media.d.ts +58 -0
  23. package/lib/types/client/gallery-merge.d.ts +15 -0
  24. package/lib/types/client/gallery-store.d.ts +23 -2
  25. package/lib/types/client/gallery-thumbs.d.ts +24 -0
  26. package/lib/types/client/gallery-view.d.ts +0 -207
  27. package/lib/types/client/gif-card.d.ts +67 -0
  28. package/lib/types/client/gif-comments.d.ts +95 -0
  29. package/lib/types/client/gif-controls.d.ts +22 -0
  30. package/lib/types/client/gif-edit.d.ts +87 -0
  31. package/lib/types/client/gif-filmstrip.d.ts +23 -0
  32. package/lib/types/client/gif-gallery.d.ts +39 -0
  33. package/lib/types/client/gif-i18n.d.ts +159 -0
  34. package/lib/types/client/gif-menus.d.ts +36 -0
  35. package/lib/types/client/gif-player.d.ts +131 -0
  36. package/lib/types/client/gif-sheet.d.ts +116 -0
  37. package/lib/types/client/gif-style.d.ts +2 -0
  38. package/lib/types/client/image-result-node.d.ts +19 -1
  39. package/lib/types/client/image-result-view.d.ts +26 -0
  40. package/lib/types/client/index.d.ts +1 -20
  41. package/lib/types/client/plugin-i18n.d.ts +375 -0
  42. package/lib/types/client/plugin-style.d.ts +1 -0
  43. package/lib/types/client/provider-pill.d.ts +9 -0
  44. package/lib/types/client/studio-helpers.d.ts +63 -0
  45. package/lib/types/client/studio-session.d.ts +54 -0
  46. package/lib/types/client/studio-style.d.ts +1 -1
  47. package/lib/types/client/studio-view.d.ts +6 -7
  48. package/lib/types/client/tl/gallery-image-transfer.d.ts +1 -1
  49. package/lib/types/config.d.ts +12 -0
  50. package/lib/types/gif-info.d.ts +32 -0
  51. package/lib/types/import-route.d.ts +11 -0
  52. package/lib/types/multipart.d.ts +29 -0
  53. package/lib/types/openai-compatible.d.ts +8 -0
  54. package/lib/types/openai-output.d.ts +20 -1
  55. package/lib/types/request-cache.d.ts +41 -0
  56. package/lib/types/shared.d.ts +57 -3
  57. package/lib/types/storage-stats.d.ts +34 -0
  58. package/lib/types/studio-route.d.ts +2 -0
  59. package/lib/types/studio.d.ts +9 -0
  60. package/lib/types/subscription/vendors/codex.d.ts +4 -3
  61. package/lib/types/thumb-route.d.ts +25 -0
  62. package/lib/types/vault-registry.d.ts +66 -0
  63. package/lib/types/vault-route.d.ts +8 -0
  64. package/lib/types/vault-store.d.ts +33 -0
  65. package/lib/types/zip-progress.d.ts +18 -0
  66. package/lib/types/zip-route.d.ts +25 -0
  67. package/lib/types/zip-stream.d.ts +43 -0
  68. package/package.json +2 -2
  69. package/lib/types/client/tl/tl-trace-bridge.d.ts +0 -55
  70. package/lib/types/client/tl/trace-playback.d.ts +0 -101
  71. package/lib/types/trace-drawing.d.ts +0 -102
  72. package/lib/types/trace-route.d.ts +0 -12
  73. package/lib/types/trace-sketch.d.ts +0 -26
  74. package/lib/types/trace-store.d.ts +0 -23
  75. package/lib/types/vendor/drawai/raster.d.ts +0 -43
  76. package/lib/types/vendor/drawai/trace-image.d.ts +0 -257
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Copy for the GIF player and the card's saved-file opener, kept apart from
3
+ * the plugin dictionary so this UI ships as its own files.
4
+ */
5
+ export declare const GIF_TEXT: {
6
+ readonly zh: {
7
+ readonly gifBadge: "GIF 动图";
8
+ readonly openInSidebar: "在右侧栏打开";
9
+ readonly notSavedVersion: "这个版本没有保存到工作区";
10
+ readonly gifControls: "GIF 播放控制";
11
+ readonly gifPlay: "播放 GIF";
12
+ readonly gifPause: "暂停 GIF";
13
+ readonly gifPlayTitle: "播放 GIF(空格)";
14
+ readonly gifPauseTitle: "暂停 GIF(空格)";
15
+ readonly gifFrames: "GIF 帧";
16
+ readonly gifFrameOf: "第 {current} 帧,共 {total} 帧";
17
+ readonly gifStripHint: "悬停预览,点击停在这一帧,← → 逐帧";
18
+ readonly gifStripHintGallery: "悬停预览,点击停在这一帧;焦点在这里时 ← → 逐帧,其它时候 ← → 翻页";
19
+ readonly gifSpeed: "播放速度";
20
+ readonly gifSpeedHint: "拖动调整播放速度(0.2×–2×),双击恢复 1×";
21
+ readonly gifFaster: "更快";
22
+ readonly gifSlower: "更慢";
23
+ readonly gifDownloadMenu: "下载";
24
+ readonly gifDownload: "下载 GIF";
25
+ readonly gifDownloadSpeed: "下载 GIF({speed}×)";
26
+ readonly gifDownloadHintOriginal: "原速动图";
27
+ readonly gifDownloadHintSpeed: "按当前播放速度";
28
+ readonly gifDownloadOriginal: "下载原速 GIF";
29
+ readonly gifDownloadOriginalHint: "不改变帧时长";
30
+ readonly gifDownloadFrame: "下载当前帧(PNG)";
31
+ readonly gifDownloadFrameHint: "第 {frame} 帧,静态图";
32
+ readonly gifCopyFrame: "复制当前帧(静态图)";
33
+ readonly gifCopyFirstFrame: "复制首帧(静态图)";
34
+ readonly gifCopiedFrame: "已复制第 {current}/{total} 帧(静态图);要动图请下载或拖出";
35
+ readonly gifCopiedFirstFrame: "已复制首帧(静态图);要动图请下载或拖出";
36
+ readonly gifCopyFailed: "复制失败";
37
+ readonly gifClose: "关闭(Esc)";
38
+ readonly gifControlsUnavailable: "此环境不支持逐帧控制,动图仍可下载";
39
+ readonly gifEdit: "编辑";
40
+ readonly gifEditChipTitle: "逐帧写修改意见,让 AI 重新画这个动图";
41
+ readonly gifEditToggle: "编辑帧";
42
+ readonly gifEditEnterTitle: "暂停在当前帧,点画面写修改意见";
43
+ readonly gifEditExitTitle: "退出编辑";
44
+ readonly gifEditTooManyFrames: "超过 16 帧的动图暂不支持按帧修改";
45
+ readonly gifEditDecodeFailed: "无法解码动图,暂不能按帧修改";
46
+ readonly gifEditDecoding: "正在解码动图帧…";
47
+ readonly gifEditNotLoaded: "动图帧尚未加载,暂不能按帧修改";
48
+ readonly gifEditHint: "在画面上点一下写评论;评论只显示在它所在的那一帧,可以换帧继续加,最后一次发送。";
49
+ readonly gifEditSheetHint: "所有帧平铺成一张网格图,点任意一格放评论,就是给那一帧写修改意见。";
50
+ readonly gifEditLayerLabel: "在第 {frame} 帧上放评论";
51
+ readonly gifEditView: "显示方式";
52
+ readonly gifGifView: "动图";
53
+ readonly gifSheetView: "全部帧";
54
+ readonly gifFrameComments: "第 {current} 帧 · 共 {count} 条评论";
55
+ readonly gifSheetComments: "共 {total} 帧 · {count} 条评论";
56
+ readonly gifCommentPlaceholder: "添加评论…";
57
+ readonly gifCommentNew: "在第 {frame} 帧添加评论";
58
+ readonly gifCommentPublish: "发表评论";
59
+ readonly gifCommentPublishTitle: "发表(Enter)";
60
+ readonly gifCommentEdit: "编辑评论 {index}";
61
+ readonly gifCommentView: "查看评论";
62
+ readonly gifCommentSheetTitle: "第 {frame} 帧:{text}";
63
+ readonly gifCommentLabel: "评论 {index}(第 {frame} 帧)";
64
+ readonly gifCommentSave: "保存";
65
+ readonly gifCommentDelete: "删除评论";
66
+ readonly gifCommentsClear: "清空";
67
+ readonly gifCommentsClearConfirm: "确定清空全部 {count} 条评论吗?";
68
+ readonly gifEditCancel: "取消";
69
+ readonly gifSend: "发送 ({count})";
70
+ readonly gifSending: "发送中…";
71
+ readonly gifEditDone: "已按评论重绘";
72
+ readonly gifEditSaveFailed: "已按评论重绘,但保存到图库失败,请重试";
73
+ readonly gifEditFailed: "按帧修改失败";
74
+ readonly gifEditFailedNotice: "按帧修改失败:{reason}";
75
+ readonly gifEditFailedHint: "通常是该渠道的图生图接口不接受这次请求(如不支持 images/edits 或 GIF 输出);可改为描述后重新生成。";
76
+ readonly gifEditStill: "渠道返回的是静态图,动画未保留;评论仍留在上一版本。";
77
+ readonly gifEditFallbackRegenerate: "改为描述后重新生成(不保留原动画)";
78
+ readonly gifEditFallbackHint: "评论已改写成文字要求附在提示词后。这会整段重新生成,不保留原动画;可继续修改后确认。";
79
+ readonly gifSheetTooLarge: "动图太大,无法生成参考拼图";
80
+ };
81
+ readonly en: {
82
+ readonly gifBadge: "Animated GIF";
83
+ readonly openInSidebar: "Open in the sidebar";
84
+ readonly notSavedVersion: "This version was not saved to the workspace";
85
+ readonly gifControls: "GIF playback controls";
86
+ readonly gifPlay: "Play GIF";
87
+ readonly gifPause: "Pause GIF";
88
+ readonly gifPlayTitle: "Play GIF (Space)";
89
+ readonly gifPauseTitle: "Pause GIF (Space)";
90
+ readonly gifFrames: "GIF frames";
91
+ readonly gifFrameOf: "Frame {current} of {total}";
92
+ readonly gifStripHint: "Hover to preview, click to stop on this frame, ← → to step";
93
+ readonly gifStripHintGallery: "Hover to preview, click to stop on this frame; ← → step frames while this has focus and turn pages otherwise";
94
+ readonly gifSpeed: "Playback speed";
95
+ readonly gifSpeedHint: "Drag to change the playback speed (0.2×–2×); double-click for 1×";
96
+ readonly gifFaster: "Faster";
97
+ readonly gifSlower: "Slower";
98
+ readonly gifDownloadMenu: "Download";
99
+ readonly gifDownload: "Download GIF";
100
+ readonly gifDownloadSpeed: "Download GIF ({speed}×)";
101
+ readonly gifDownloadHintOriginal: "Animation at its own speed";
102
+ readonly gifDownloadHintSpeed: "At the current playback speed";
103
+ readonly gifDownloadOriginal: "Download original-speed GIF";
104
+ readonly gifDownloadOriginalHint: "Frame timing unchanged";
105
+ readonly gifDownloadFrame: "Download frame (PNG)";
106
+ readonly gifDownloadFrameHint: "Frame {frame}, still image";
107
+ readonly gifCopyFrame: "Copy frame (still)";
108
+ readonly gifCopyFirstFrame: "Copy first frame (still)";
109
+ readonly gifCopiedFrame: "Frame {current} of {total} copied as a still; download or drag out for the GIF";
110
+ readonly gifCopiedFirstFrame: "First frame copied as a still; download or drag out for the GIF";
111
+ readonly gifCopyFailed: "Copy failed";
112
+ readonly gifClose: "Close (Esc)";
113
+ readonly gifControlsUnavailable: "Frame-by-frame controls are not available here; the GIF can still be downloaded";
114
+ readonly gifEdit: "Edit";
115
+ readonly gifEditChipTitle: "Leave notes on frames and have the AI redraw this animation";
116
+ readonly gifEditToggle: "Edit frames";
117
+ readonly gifEditEnterTitle: "Pause on this frame and click the picture to leave notes";
118
+ readonly gifEditExitTitle: "Leave editing";
119
+ readonly gifEditTooManyFrames: "Animations with more than 16 frames cannot be edited by frame yet";
120
+ readonly gifEditDecodeFailed: "The animation could not be decoded, so it cannot be edited by frame";
121
+ readonly gifEditDecoding: "Decoding the animation frames…";
122
+ readonly gifEditNotLoaded: "The animation frames are not loaded yet, so it cannot be edited by frame";
123
+ readonly gifEditHint: "Click the picture to leave a note. A note shows only on its own frame; switch frames to add more, then send them all at once.";
124
+ readonly gifEditSheetHint: "Every frame laid out in one grid: click a cell to leave a note on that frame.";
125
+ readonly gifEditLayerLabel: "Place a note on frame {frame}";
126
+ readonly gifEditView: "View";
127
+ readonly gifGifView: "GIF";
128
+ readonly gifSheetView: "All frames";
129
+ readonly gifFrameComments: "Frame {current} · {count} notes";
130
+ readonly gifSheetComments: "{total} frames · {count} notes";
131
+ readonly gifCommentPlaceholder: "Add a note…";
132
+ readonly gifCommentNew: "New note on frame {frame}";
133
+ readonly gifCommentPublish: "Post note";
134
+ readonly gifCommentPublishTitle: "Post (Enter)";
135
+ readonly gifCommentEdit: "Edit note {index}";
136
+ readonly gifCommentView: "View note";
137
+ readonly gifCommentSheetTitle: "Frame {frame}: {text}";
138
+ readonly gifCommentLabel: "Note {index} (frame {frame})";
139
+ readonly gifCommentSave: "Save";
140
+ readonly gifCommentDelete: "Delete note";
141
+ readonly gifCommentsClear: "Clear";
142
+ readonly gifCommentsClearConfirm: "Clear all {count} notes?";
143
+ readonly gifEditCancel: "Cancel";
144
+ readonly gifSend: "Send ({count})";
145
+ readonly gifSending: "Sending…";
146
+ readonly gifEditDone: "Redrawn from your notes";
147
+ readonly gifEditSaveFailed: "Redrawn from your notes, but saving to the gallery failed. Please retry.";
148
+ readonly gifEditFailed: "Frame edit failed";
149
+ readonly gifEditFailedNotice: "Frame edit failed: {reason}";
150
+ readonly gifEditFailedHint: "Usually the channel's image-editing endpoint did not accept this request (for example, no images/edits or no GIF output); you can regenerate from a description instead.";
151
+ readonly gifEditStill: "The channel returned a still image, so the animation was not kept; your notes stay on the previous version.";
152
+ readonly gifEditFallbackRegenerate: "Regenerate from a description instead (the current animation is not kept)";
153
+ readonly gifEditFallbackHint: "Your notes were rewritten as text requirements after the prompt. This regenerates the whole animation without keeping the current one; edit further, then confirm.";
154
+ readonly gifSheetTooLarge: "The animation is too large to build a reference sheet";
155
+ };
156
+ };
157
+ export type GifTextKey = keyof typeof GIF_TEXT.zh;
158
+ export type GifT = (key: GifTextKey, params?: Record<string, string>) => string;
159
+ export declare function gifT(lang: 'zh' | 'en'): GifT;
@@ -0,0 +1,36 @@
1
+ import type { GifT } from './gif-i18n.js';
2
+ import { type GifPlayer } from './gif-player.js';
3
+ /**
4
+ * The speed slider: a native range for keys and assistive tech, drawn as a
5
+ * vertical track. It carries no numbers; the trigger shows the value.
6
+ */
7
+ export declare function GifSpeedPanel({ speed, t, onChange }: {
8
+ speed: number;
9
+ t: GifT;
10
+ onChange(speed: number): void;
11
+ }): import("react").JSX.Element;
12
+ /** The speed trigger and its panel: a chip on the card, a button in the lightbox bar. */
13
+ export declare function GifSpeedControl({ player, t, variant, onOpenChange }: {
14
+ player: GifPlayer;
15
+ t: GifT;
16
+ variant: 'chip' | 'bar';
17
+ onOpenChange?: (open: boolean) => void;
18
+ }): import("react").JSX.Element;
19
+ export type GifDownloadKind = 'gif' | 'original' | 'frame';
20
+ export interface GifDownloadItem {
21
+ kind: GifDownloadKind;
22
+ label: string;
23
+ hint: string;
24
+ disabled: boolean;
25
+ }
26
+ /** The GIF at the speed it plays, the original timing when that differs, and the frame on screen (one-based). */
27
+ export declare function gifDownloadItems(t: GifT, speed: number, framesReady: boolean, frame: number): GifDownloadItem[];
28
+ export interface GifDownloadMenuProps {
29
+ player: GifPlayer;
30
+ t: GifT;
31
+ /** The card's hover toolbar, the lightbox's top-right actions, or the gallery preview's bottom bar. */
32
+ variant: 'toolbar' | 'lightbox' | 'gallery';
33
+ onDownload(kind: GifDownloadKind): void;
34
+ onOpenChange?: (open: boolean) => void;
35
+ }
36
+ export declare function GifDownloadMenu({ player, t, variant, onDownload, onOpenChange }: GifDownloadMenuProps): import("react").JSX.Element;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Frame-accurate GIF playback for the conversation card: a small external
3
+ * store that owns the decoded frames and the clock, a stage that paints the
4
+ * current frame, and a control bar. Frames are decoded only while a stage is
5
+ * visible and only within a memory budget; otherwise the GIF plays natively in
6
+ * an <img>, which still animates but cannot pause, seek or change speed.
7
+ */
8
+ import { type DragEvent, type MouseEvent } from 'react';
9
+ import { type GifInfo } from '../gif-info.js';
10
+ import type { GifT } from './gif-i18n.js';
11
+ /** Range and step of the speed control: one decimal is enough to fine-tune around 1x. */
12
+ export declare const GIF_SPEED_MIN = 0.2;
13
+ export declare const GIF_SPEED_MAX = 2;
14
+ export declare const GIF_SPEED_STEP = 0.1;
15
+ export declare const DECODE_TOTAL_BUDGET_BYTES: number;
16
+ export declare function nextFrameIndex(index: number, total: number, delta?: number): number;
17
+ /** Delays are rounded to the 10 ms ticks a retimed download stores, so the preview plays at the file's speed. */
18
+ export declare function scheduledDelayMs(delayMs: number, speed: number): number;
19
+ /** A speed on the control's grid and within its range; anything unreadable is 1x. */
20
+ export declare function snapSpeed(speed: number): number;
21
+ export declare function formatSpeed(speed: number): string;
22
+ /**
23
+ * The delay a browser plays a frame for: its own decoder's duration when it
24
+ * reports one, else the header delay; either way a delay of at most 10 ms is
25
+ * shown for 100 ms, as the <img> element and the retimed download do.
26
+ */
27
+ export declare function playedDelayMs(durationUs: number | null | undefined, headerMs: number): number;
28
+ type GifKeyboardAction = 'toggle' | 'previous' | 'next' | 'first' | 'last';
29
+ /**
30
+ * The lightbox shortcut for a key, given the element that holds focus. A
31
+ * focused control keeps only the keys it handles itself (a button its Space
32
+ * and Enter, the frame scrubber its arrows and Home/End), so stepping still
33
+ * works after the mouse has clicked a button, while a text field keeps all.
34
+ */
35
+ export declare function gifKeyboardAction(key: string, target: {
36
+ tagName: string;
37
+ type?: string | undefined;
38
+ } | null): GifKeyboardAction | undefined;
39
+ /** File-name suffix for a download at a non-default speed. */
40
+ export declare function speedSuffix(speed: number): string;
41
+ /** "Download GIF", naming the speed only when it is not the file's own. */
42
+ export declare function gifDownloadLabel(t: GifT, speed: number): string;
43
+ /** Judged from the header before anything is decoded: the GIF alone, and together with what other cards already hold; a header without a screen size cannot be judged. */
44
+ export declare function withinDecodeBudget(info: Pick<GifInfo, 'frames' | 'width' | 'height'>, usedBytes: number): boolean;
45
+ type GifPlayerStatus = 'native' | 'decoding' | 'ready' | 'fallback';
46
+ export interface GifPlayerSnapshot {
47
+ readonly status: GifPlayerStatus;
48
+ readonly info: GifInfo | undefined;
49
+ readonly index: number;
50
+ readonly playing: boolean;
51
+ /** A pointer is previewing frames on the strip: the clock holds, play state is kept. */
52
+ readonly scrubbing: boolean;
53
+ readonly speed: number;
54
+ /** Composited frames while `ready`; empty otherwise. */
55
+ readonly frames: readonly ImageBitmap[];
56
+ readonly delaysMs: readonly number[];
57
+ }
58
+ interface DecodedGif {
59
+ frames: ImageBitmap[];
60
+ delaysMs: number[];
61
+ }
62
+ /** Browser services the store relies on; tests inject stand-ins. */
63
+ export interface GifPlayerDeps {
64
+ /** Resolves undefined when this environment cannot decode frame by frame. */
65
+ decode(blob: Blob, info: GifInfo): Promise<DecodedGif | undefined>;
66
+ encodeFrame(frame: ImageBitmap): Promise<Blob | undefined>;
67
+ hidden(): boolean;
68
+ setTimeout(fn: () => void, ms: number): unknown;
69
+ clearTimeout(handle: unknown): void;
70
+ }
71
+ export interface GifPlayer {
72
+ readonly blob: Blob;
73
+ /** Object URL of the raw GIF for native playback and "open in new tab". */
74
+ readonly url: string | undefined;
75
+ /** The raw bytes once read; a drag-out needs them synchronously. */
76
+ bytes(): Uint8Array | undefined;
77
+ subscribe(listener: () => void): () => void;
78
+ getSnapshot(): GifPlayerSnapshot;
79
+ /** Each stage reports its own viewport state; frames exist while any stage is visible. */
80
+ setVisible(stage: unknown, visible: boolean): void;
81
+ play(): void;
82
+ pause(): void;
83
+ toggle(): void;
84
+ /** Seeking and stepping pause playback, so the chosen frame stays. */
85
+ seek(index: number): void;
86
+ /** Show a frame while the pointer is over the strip, without changing play state. */
87
+ scrub(index: number): void;
88
+ /** Leave the strip: playback goes on from the frame shown; a paused player returns to the frame it chose. */
89
+ endScrub(): void;
90
+ step(delta: number): void;
91
+ first(): void;
92
+ last(): void;
93
+ /** Takes effect on the next tick; nothing is re-decoded. */
94
+ setSpeed(speed: number): void;
95
+ /** Encode the frame on screen as PNG, leaving playback as it is; undefined until frames are decoded. */
96
+ captureFrame(): Promise<Blob | undefined>;
97
+ dispose(): void;
98
+ }
99
+ export declare function createGifPlayer(blob: Blob, overrides?: Partial<GifPlayerDeps>): GifPlayer;
100
+ /**
101
+ * Paint one composited frame on the stage canvas. The canvas is cleared first:
102
+ * a frame can hold transparent pixels after disposal compositing, and
103
+ * source-over would leave the previous frame showing through them. A bitmap
104
+ * released between render and effect has zero size; drawing it would throw,
105
+ * so it is skipped.
106
+ */
107
+ export declare function paintGifFrame(canvas: HTMLCanvasElement, frame: ImageBitmap): void;
108
+ /** Own a player for the current GIF blob; a new blob replaces and disposes the previous player. */
109
+ export declare function useGifPlayer(blob: Blob | undefined): GifPlayer | undefined;
110
+ /** Status alone, so the card re-renders on readiness rather than on every frame. */
111
+ export declare function useGifStatus(player: GifPlayer | undefined): GifPlayerStatus | undefined;
112
+ export declare function useGifSpeed(player: GifPlayer | undefined): number | undefined;
113
+ export declare function useGifPlaying(player: GifPlayer): boolean;
114
+ /** Whether the file's bytes are read: the speed and a speed-named download need them, frames do not. */
115
+ export declare function useGifReadable(player: GifPlayer | undefined): boolean;
116
+ /** Every change, including each frame; only components that draw the frame should use it. */
117
+ export declare function useGifSnapshot(player: GifPlayer): GifPlayerSnapshot;
118
+ interface GifStageProps {
119
+ player: GifPlayer;
120
+ /** The card, the lightbox and the gallery preview are separate stages, each reporting its own visibility. */
121
+ stage: 'card' | 'lightbox' | 'gallery';
122
+ className: string;
123
+ alt: string;
124
+ /** Still image for a paused player without frames (re-decoding after a scroll), instead of the natively animating GIF. */
125
+ poster?: string | undefined;
126
+ onClick?: (event: MouseEvent<HTMLElement>) => void;
127
+ onDragStart?: (event: DragEvent<HTMLElement>) => void;
128
+ }
129
+ /** Paints the current frame on a canvas once decoded; the raw GIF plays in an <img> until then or when decoding is not possible. */
130
+ export declare function GifStage({ player, stage, className, alt, poster, onClick, onDragStart }: GifStageProps): import("react").JSX.Element;
131
+ export {};
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The contact sheet a frame-comment edit sends as its reference image, and the
3
+ * prompt and request built around it. An edits endpoint takes a still, so
4
+ * every frame of the animation is tiled into one near-square grid in reading
5
+ * order; the prompt explains the grid and anchors each note inside its frame.
6
+ * Pure except for the canvas work, which takes injectable canvas and encoder
7
+ * stand-ins.
8
+ */
9
+ import { type StudioGenerateRequest } from '../shared.js';
10
+ import { type RegeneratableImage } from './conversation-regenerate.js';
11
+ export declare const GIF_COMMENT_MAX_CHARS = 500;
12
+ export declare const GIF_COMMENTS_MAX = 32;
13
+ export interface GifComment {
14
+ /** Creation order; also the number on the pin. */
15
+ id: number;
16
+ /** Zero-based frame index. */
17
+ frame: number;
18
+ /** Position inside that frame, in percent of its width and height from the top-left corner. */
19
+ x: number;
20
+ y: number;
21
+ text: string;
22
+ }
23
+ export interface GifSheetPlan {
24
+ cols: number;
25
+ rows: number;
26
+ /** Cell size in sheet pixels; the frame size unless the sheet had to shrink. */
27
+ cellW: number;
28
+ cellH: number;
29
+ width: number;
30
+ height: number;
31
+ /** Trailing empty cells of the grid. */
32
+ blanks: number;
33
+ frames: number;
34
+ }
35
+ export interface SheetEncodeAttempt {
36
+ scale: number;
37
+ type: 'image/png' | 'image/jpeg';
38
+ quality: number;
39
+ }
40
+ export type SheetEncoder = (canvas: HTMLCanvasElement, attempt: SheetEncodeAttempt) => Promise<Blob | undefined>;
41
+ export interface EncodedSheet {
42
+ blob: Blob;
43
+ mediaType: 'image/png' | 'image/jpeg';
44
+ plan: GifSheetPlan;
45
+ }
46
+ /**
47
+ * Near-square grid: cols = ceil(sqrt(n)). The cells shrink, snapped down to
48
+ * 16 px, only when the sheet's longest edge would exceed the provider limit.
49
+ */
50
+ export declare function planGifSheet(frames: number, frameW: number, frameH: number, maxEdge?: number): GifSheetPlan;
51
+ /** The size to show the sheet at: the full sheet scaled into the display edge, so the editor does not hold a second full-size copy. */
52
+ export declare function sheetViewSize(plan: Pick<GifSheetPlan, 'width' | 'height'>, maxEdge?: number): {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ /** A point inside one frame, as the same point on the whole sheet (percent, one decimal). */
57
+ export declare function sheetPinPosition(comment: Pick<GifComment, 'frame' | 'x' | 'y'>, plan: GifSheetPlan): {
58
+ x: number;
59
+ y: number;
60
+ };
61
+ /** Paint the frames into their cells on a white sheet; blank cells stay white. */
62
+ export declare function drawGifSheet(frames: readonly CanvasImageSource[], plan: GifSheetPlan, createCanvas?: () => HTMLCanvasElement): HTMLCanvasElement;
63
+ /**
64
+ * First attempt that fits both the host limit and a modest request budget.
65
+ * The returned plan is scaled with the attempt, so the prompt can name the
66
+ * cell size that was actually sent.
67
+ */
68
+ export declare function encodeSheetWithinLimit(canvas: HTMLCanvasElement, plan: GifSheetPlan, maxBytes: number, encode?: SheetEncoder): Promise<EncodedSheet>;
69
+ /**
70
+ * The edit prompt: the opening instruction, the sheet layout as sent (its
71
+ * cells may be smaller than the frames) and the frames to produce at the
72
+ * animation's own size, then the notes grouped by frame the way ChatGPT
73
+ * writes them: a `Frame #n:` heading and one `(x: X%, y: Y%): note` line each.
74
+ */
75
+ export declare function composeGifEditPrompt(comments: readonly GifComment[], plan: GifSheetPlan, frame: {
76
+ width: number;
77
+ height: number;
78
+ }): string;
79
+ export interface GifEditRequestInput {
80
+ /** The version being edited: its channel, model, output and the description it was drawn from. */
81
+ image: RegeneratableImage & {
82
+ prompt: string;
83
+ };
84
+ /** The editing model of the channel, as the workbench would pick it. */
85
+ model: string;
86
+ comments: readonly GifComment[];
87
+ /** The encoded sheet and the plan it was encoded at (scaled with the encoding attempt). */
88
+ sheet: {
89
+ mediaType: 'image/png' | 'image/jpeg';
90
+ data: string;
91
+ plan: GifSheetPlan;
92
+ };
93
+ /** The decoded frames: their size and the delays they play at, as captured when the editor opened. */
94
+ frame: {
95
+ width: number;
96
+ height: number;
97
+ delaysMs: readonly number[];
98
+ };
99
+ /** Playback speed; the delay asked for follows it, like a download does. */
100
+ speed: number;
101
+ }
102
+ /**
103
+ * The workbench edit that redraws the animation from the notes: the sheet as
104
+ * the only reference, and a GIF with the sheet's frame count, the delay the
105
+ * frames play at retimed by the speed, and the frame size the contract takes.
106
+ * The prompt is the sheet instruction; the card files the new version under
107
+ * the animation's own description instead.
108
+ */
109
+ export declare function gifEditRequest({ image, model, comments, sheet, frame, speed }: GifEditRequestInput): StudioGenerateRequest;
110
+ /**
111
+ * The same notes for a plain regeneration, which sees neither the previous
112
+ * animation nor the sheet: no coordinates, the frame only as a time hint.
113
+ */
114
+ export declare function composeFallbackPrompt(originalPrompt: string, comments: readonly GifComment[], frameTotal: number): string;
115
+ /** The frame delay to ask for: the animation's own delay retimed by the playback speed, within the provider range, in 10 ms steps. */
116
+ export declare function gifEditFrameDurationMs(baseMs: number, speed: number): number | undefined;
@@ -0,0 +1,2 @@
1
+ /** Styles of the GIF player (card overlays, the lightbox player, its menus, the gallery hover) and of the card's saved-file link. */
2
+ export declare const GIF_STYLE = "\n/* The host draws corners as superellipses; circles and pills here say corner-shape:round, as the host's own do. */\n.dsh-ig-savedto-open{appearance:none;display:inline-block;max-width:calc(100% - 5em);margin:0;padding:0;border:0;background:none;font:inherit;color:var(--dsw-alias-link,#3b6cf0);text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;cursor:pointer}\n.dsh-ig-savedto-open:hover{text-decoration:underline}\n.dsh-ig-container.is-gif-open{overflow:visible}\n.dsh-ig-toolbar.is-pinned{opacity:1;pointer-events:auto}\n/* Above the chips only while its own menu is open, so it never covers the speed panel. */\n.dsh-ig-toolbar:has(.dsh-ig-dl-menu){z-index:4}\n.dsh-ig-toolbar .dsh-ig-tool-btn:disabled{opacity:.4;cursor:default;background:transparent}\n.dsh-ig-gif-badge{margin-left:8px;vertical-align:middle;font-size:11px;font-weight:600}\n.dsh-ig-gif-chip-slot{position:absolute;left:12px;bottom:12px;z-index:3;line-height:normal}\n.dsh-ig-gif-chip{corner-shape:round;appearance:none;border:0;min-width:44px;height:36px;padding:0 10px;border-radius:999px;background:rgba(17,20,26,0.6);color:#fff;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;cursor:pointer;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}\n.dsh-ig-gif-chip:hover:not(:disabled){background:rgba(17,20,26,0.8)}\n.dsh-ig-gif-chip:disabled{opacity:.55;cursor:default}\n.dsh-ig-speed{position:relative;display:inline-flex}\n.dsh-ig-speed-panel{corner-shape:round;position:absolute;bottom:calc(100% + 10px);left:50%;transform:translateX(-50%);width:52px;height:212px;box-sizing:border-box;padding:10px 0;border-radius:26px;background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-tertiary,#8a8f99);box-shadow:0 10px 30px rgba(15,23,42,0.18),0 0 0 1px rgba(15,23,42,0.06);display:flex;flex-direction:column;align-items:center;gap:8px;z-index:20;line-height:normal}\n.dsh-ig-speed-bar .dsh-ig-speed-panel{bottom:calc(100% + 12px);background:rgba(32,35,42,0.96);color:rgba(255,255,255,0.6);box-shadow:0 10px 30px rgba(0,0,0,0.45)}\n.dsh-ig-speed-bar .dsh-ig-speed-rail{background:rgba(255,255,255,0.18)}\n.dsh-ig-speed-bar .dsh-ig-speed-fill{background:#5b8cff}\n.dsh-ig-speed-bar .dsh-ig-speed-one{background:rgba(255,255,255,0.45)}\n.dsh-ig-speed-chip .dsh-ig-speed-panel{left:0;transform:none}\n.dsh-ig-speed-icon{display:flex}\n.dsh-ig-speed-track{position:relative;width:28px;height:150px}\n.dsh-ig-speed-rail,.dsh-ig-speed-fill{corner-shape:round;position:absolute;left:11px;width:6px;border-radius:3px}\n.dsh-ig-speed-rail{top:0;bottom:0;background:rgba(127,127,127,0.25)}\n.dsh-ig-speed-fill{bottom:0;height:var(--dsh-ig-speed-pct);background:var(--dsw-alias-state-business-primary,#3b6cf0)}\n.dsh-ig-speed-one{position:absolute;left:7px;width:14px;height:2px;border-radius:1px;top:calc(var(--dsh-ig-speed-one) - 1px);background:rgba(127,127,127,0.55)}\n.dsh-ig-speed-knob{corner-shape:round;position:absolute;left:4px;width:20px;height:20px;border-radius:50%;bottom:calc(var(--dsh-ig-speed-pct) - 10px);background:#fff;box-shadow:0 1px 4px rgba(15,23,42,0.35);pointer-events:none}\n.dsh-ig-speed-track:focus-within .dsh-ig-speed-knob{box-shadow:0 0 0 3px rgba(59,108,240,0.45),0 1px 4px rgba(15,23,42,0.35)}\n.dsh-ig-speed-input{position:absolute;inset:0;width:28px;height:150px;margin:0;opacity:0;writing-mode:vertical-lr;direction:rtl;cursor:pointer}\n.dsh-ig-dl{position:relative;display:inline-flex}\n.dsh-ig-dl-menu{position:absolute;top:calc(100% + 8px);left:0;width:236px;box-sizing:border-box;padding:6px;border-radius:12px;background:var(--dsw-alias-bg-layer-3,#fff);box-shadow:0 10px 30px rgba(15,23,42,0.18),0 0 0 1px rgba(15,23,42,0.06);z-index:20;line-height:normal}\n.dsh-ig-dl-lightbox .dsh-ig-dl-menu{left:auto;right:0;box-shadow:0 10px 30px rgba(0,0,0,0.4)}\n.dsh-ig-dl-item{appearance:none;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;min-height:44px;padding:8px 14px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#1f2329);font:inherit;font-size:14px;text-align:left;cursor:pointer}\n.dsh-ig-dl-item>span:first-child{font-weight:500}\n.dsh-ig-dl-item:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2,#f2f4f7)}\n.dsh-ig-dl-item:disabled{opacity:.45;cursor:default}\n.dsh-ig-dl-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99)}\n/* The lightbox and the gallery preview are always dark: their menus stay light whatever the host theme. */\n.dsh-ig-dl-lightbox .dsh-ig-dl-menu,.dsh-ig-dl-gallery .dsh-ig-dl-menu{background:#fff}\n.dsh-ig-dl-lightbox .dsh-ig-dl-item,.dsh-ig-dl-gallery .dsh-ig-dl-item{color:#1f2329}\n.dsh-ig-dl-lightbox .dsh-ig-dl-item:hover:not(:disabled),.dsh-ig-dl-gallery .dsh-ig-dl-item:hover:not(:disabled){background:#f2f4f7}\n.dsh-ig-dl-lightbox .dsh-ig-dl-hint,.dsh-ig-dl-gallery .dsh-ig-dl-hint{color:#8a8f99}\n.dsh-ig-gif-actions{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:10px;z-index:10}\n.dsh-ig-gif-action{corner-shape:round;appearance:none;border:0;height:40px;min-width:40px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,0.12);color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:4px;font-size:13px;cursor:pointer}\n.dsh-ig-gif-action:hover{background:rgba(255,255,255,0.22)}\n.dsh-ig-gif-toast{position:absolute;top:72px;right:20px;font-size:12px;color:rgba(255,255,255,0.8);z-index:10}\n/* The lightbox is always dark, so its accents use a fixed blue: the host's brand colour is neutral (near-black, or near-white in its dark theme). */\n.dsh-ig-gif-dock{position:absolute;bottom:28px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:stretch;gap:10px;max-width:calc(100vw - 48px);z-index:10;cursor:default}\n.dsh-ig-gif-bar{corner-shape:round;display:flex;align-items:center;gap:10px;padding:6px;border-radius:999px;background:rgba(255,255,255,0.1);box-shadow:0 0 0 1px rgba(255,255,255,0.08);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-sizing:border-box;max-width:calc(100vw - 48px);color:#fff;line-height:normal}\n/* No z-index: the gallery's bottom bar, later in the page, paints over this one, so its download menu opens on top. */\n.dsh-ig-gif-bar-gallery{position:absolute;bottom:128px;left:50%;transform:translateX(-50%)}\n.dsh-ig-gif-play{corner-shape:round;appearance:none;flex:none;width:44px;height:44px;border:0;border-radius:50%;background:#fff;color:#0d0f13;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}\n.dsh-ig-gif-play:disabled{opacity:.5;cursor:default}\n.dsh-ig-gif-bar .dsh-ig-speed-trigger{corner-shape:round;appearance:none;min-width:56px;height:44px;padding:0 12px;border:0;border-radius:999px;background:rgba(255,255,255,0.12);color:#fff;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;cursor:pointer}\n.dsh-ig-gif-bar .dsh-ig-speed-trigger:disabled{opacity:.5;cursor:default}\n.dsh-ig-gif-strip{position:relative;flex:0 1 auto;width:350px;min-width:160px;height:44px;border-radius:8px;overflow:hidden;background:rgba(255,255,255,0.12);cursor:pointer;touch-action:none;outline:none}\n.dsh-ig-gif-strip:focus-visible{box-shadow:0 0 0 2px #fff}\n.dsh-ig-gif-strip[aria-disabled=\"true\"]{cursor:default;animation:dsh-ig-gif-pulse 1.2s ease-in-out infinite}\n.dsh-ig-gif-strip canvas{display:block;width:100%;height:100%}\n.dsh-ig-gif-strip-head{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:#5b8cff;pointer-events:none}\n@keyframes dsh-ig-gif-pulse{50%{opacity:.6}}\n.dsh-ig-gif-strip-mark{corner-shape:round;position:absolute;top:3px;width:7px;height:7px;margin-left:-3.5px;border-radius:50%;background:#5b8cff;box-shadow:0 0 0 1.5px #fff;pointer-events:none}\n/* The frame stays clear of the bars under it: it is lifted by their measured height (--dsh-ig-gif-dock-h) and sized to fit above them. */\n.dsh-ig-gif-wrap{position:relative;max-height:none;margin-bottom:calc(var(--dsh-ig-gif-dock-h,56px) + 52px)}\n.dsh-ig-gif-wrap .dsh-ig-lightbox-img,.dsh-ig-gif-sheet{display:block;max-height:calc(100vh - var(--dsh-ig-gif-dock-h,56px) - 192px)}\n.dsh-ig-gif-wrap.is-editing{border-radius:10px;box-shadow:0 0 0 3px #3b6cf0}\n.dsh-ig-gif-wrap.is-sheet .dsh-ig-gif-stage{position:absolute;inset:0;overflow:hidden;opacity:0;pointer-events:none}\n.dsh-ig-gif-sheet{display:block;max-width:86vw;border-radius:10px;background:#1b1e24}\n.dsh-ig-gif-edit-chip-slot{position:absolute;right:12px;bottom:12px;z-index:3;line-height:normal}\n.dsh-ig-container:has(.dsh-ig-version-nav) .dsh-ig-gif-edit-chip-slot{bottom:48px}\n.dsh-ig-gif-edit-chip{display:inline-flex;align-items:center;gap:6px;padding:0 12px}\n.dsh-ig-gif-edit-toggle{corner-shape:round;appearance:none;flex:none;height:44px;padding:0 18px;border-radius:999px;border:1.5px solid rgba(255,255,255,0.45);background:transparent;color:#fff;font:inherit;font-size:14px;font-weight:600;cursor:pointer}\n.dsh-ig-gif-edit-toggle.is-active{background:#3b6cf0;border-color:#3b6cf0}\n.dsh-ig-gif-edit-toggle:disabled{opacity:.5;cursor:default}\n/* The edit bar takes the player bar's width rather than widening it, and wraps its text. */\n.dsh-ig-gif-edit{contain:inline-size;box-sizing:border-box;padding:10px 12px;border-radius:14px;background:rgba(255,255,255,0.08);color:#fff;display:grid;gap:8px;line-height:normal}\n.dsh-ig-gif-edit-hint{margin:0;font-size:12px;line-height:1.45;color:rgba(255,255,255,0.6)}\n.dsh-ig-gif-edit-row{display:flex;align-items:center;gap:8px}\n.dsh-ig-gif-edit-counter{flex:1;min-width:0;font-size:13px;color:rgba(255,255,255,0.85);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.dsh-ig-gif-view{corner-shape:round;display:flex;flex:none;padding:3px;border-radius:999px;background:rgba(255,255,255,0.08)}\n.dsh-ig-gif-view button{corner-shape:round;appearance:none;height:32px;padding:0 12px;border:0;border-radius:999px;background:transparent;font:inherit;font-size:13px;color:rgba(255,255,255,0.7);cursor:pointer}\n.dsh-ig-gif-view button[aria-pressed=\"true\"]{background:rgba(255,255,255,0.18);color:#fff;font-weight:600}\n.dsh-ig-gif-edit-ghost,.dsh-ig-gif-edit-send{corner-shape:round;appearance:none;flex:none;height:36px;border-radius:999px;font:inherit;font-size:13px;cursor:pointer}\n.dsh-ig-gif-edit-ghost{padding:0 12px;border:1px solid rgba(255,255,255,0.25);background:transparent;color:#fff}\n.dsh-ig-gif-edit-send{padding:0 14px;border:0;background:#3b6cf0;color:#fff;font-weight:600}\n.dsh-ig-gif-edit-ghost:disabled,.dsh-ig-gif-edit-send:disabled{opacity:.5;cursor:default}\n.dsh-ig-gif-edit-notice,.dsh-ig-gif-edit-error{display:grid;gap:6px;justify-items:start;padding:8px 10px;border-radius:8px;font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word}\n.dsh-ig-gif-edit-notice{margin:6px 0;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,0.05));color:var(--dsw-alias-label-secondary,inherit)}\n.dsh-ig-gif-edit-error{background:rgba(239,68,68,0.16);color:#fca5a5}\n/* The cursor is the pin it places, its sharp corner on the point, as ChatGPT draws it. */\n.dsh-ig-gif-edit-layer{position:absolute;inset:0;z-index:3;margin:0;padding:0;border:0;border-radius:10px;background:transparent;cursor:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M2 22V12A10 10 0 0 1 22 12A10 10 0 0 1 12 22Z' fill='%233b6cf0' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E\") 2 22,crosshair}\n/* While a note is being written, a click on the picture only closes it, so the cursor is the plain one. */\n.dsh-ig-gif-wrap:has(.dsh-ig-gif-note.is-new,.dsh-ig-gif-note.is-edit) :is(.dsh-ig-gif-edit-layer,.dsh-ig-gif-sheet-cell){cursor:default}\n.dsh-ig-gif-edit-layer:focus-visible{outline:2px solid #fff;outline-offset:-2px}\n.dsh-ig-gif-sheet-cell{position:absolute;z-index:3;box-sizing:border-box;margin:0;padding:0;border:0;background:transparent;outline:1px solid #0d0f13;outline-offset:-1px;cursor:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M2 22V12A10 10 0 0 1 22 12A10 10 0 0 1 12 22Z' fill='%233b6cf0' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E\") 2 22,crosshair}\n.dsh-ig-gif-sheet-cell.is-current{outline:2px solid #3b6cf0;outline-offset:-2px}\n.dsh-ig-gif-sheet-cell.is-blank{cursor:default;background:#1b1e24}\n.dsh-ig-gif-sheet-cell:focus-visible{outline:2px solid #fff;outline-offset:-2px}\n.dsh-ig-gif-sheet-cell span{position:absolute;left:4px;top:4px;padding:1px 5px;border-radius:4px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;font-weight:600;line-height:1.4;pointer-events:none}\n.dsh-ig-gif-pin{corner-shape:round;position:absolute;z-index:4;min-width:28px;height:28px;margin:-28px 0 0;box-sizing:border-box;padding:0 7px;border:2px solid #fff;border-radius:14px 14px 14px 0;background:#3b6cf0;color:#fff;font:inherit;font-size:13px;font-weight:700;line-height:24px;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.4);cursor:pointer}\n.dsh-ig-gif-pin.is-small{min-width:20px;height:20px;margin-top:-20px;padding:0 4px;border-width:1.5px;border-radius:10px 10px 10px 0;font-size:11px;line-height:17px}\n.dsh-ig-gif-pin.is-active{box-shadow:0 0 0 3px rgba(59,108,240,0.55),0 2px 6px rgba(0,0,0,0.4)}\n.dsh-ig-gif-pin.is-draft{pointer-events:none}\n.dsh-ig-gif-note{corner-shape:round;position:absolute;z-index:5;width:292px;max-width:calc(100vw - 32px);box-sizing:border-box;background:#2f2f2f;color:#fff;box-shadow:0 12px 32px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.1);transform:translate(30px,-36px);line-height:normal;cursor:default}\n.dsh-ig-gif-note:focus{outline:none}\n.dsh-ig-gif-note.is-left{transform:translate(calc(-100% - 8px),-36px)}\n.dsh-ig-gif-note.is-up{transform:translate(30px,calc(-100% + 8px))}\n.dsh-ig-gif-note.is-left.is-up{transform:translate(calc(-100% - 8px),calc(-100% + 8px))}\n.dsh-ig-gif-note.is-new{height:44px;display:flex;align-items:center;gap:8px;padding:0 4px 0 18px;border-radius:999px}\n.dsh-ig-gif-note.is-edit{padding:4px 6px 6px 16px;border-radius:22px}\n.dsh-ig-gif-note.is-view{min-height:44px;display:flex;align-items:center;padding:10px 18px;border-radius:22px}\n.dsh-ig-gif-note.is-view p{margin:0;flex:1;font-size:15px;line-height:1.5;word-break:break-word}\n.dsh-ig-gif-note-field{height:40px;display:flex;align-items:center}\n.dsh-ig-gif-note-input{flex:1;min-width:0;width:100%;height:36px;margin:0;padding:0;border:0;outline:0;background:transparent;color:#fff;font:inherit;font-size:15px}\n.dsh-ig-gif-note-input::placeholder{color:rgba(255,255,255,0.5)}\n.dsh-ig-gif-note-actions{display:flex;align-items:center;gap:6px}\n.dsh-ig-gif-note-actions span{flex:1}\n.dsh-ig-gif-note-icon,.dsh-ig-gif-note-post{corner-shape:round;appearance:none;flex:none;width:36px;height:36px;margin:0;padding:0;border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer}\n.dsh-ig-gif-note-icon{margin-left:-8px;background:transparent;color:#fff}\n.dsh-ig-gif-note-icon:hover{background:rgba(255,255,255,0.1)}\n.dsh-ig-gif-note-post{background:#fff;color:#1f2329}\n.dsh-ig-gif-note-post:disabled{background:#8a8a8a;color:#2f2f2f;cursor:default}\n.dsh-ig-gif-note-ghost,.dsh-ig-gif-note-save{corner-shape:round;appearance:none;flex:none;height:36px;border-radius:999px;font:inherit;font-size:14px;cursor:pointer}\n.dsh-ig-gif-note-ghost{padding:0 14px;border:1px solid rgba(255,255,255,0.2);background:rgba(255,255,255,0.06);color:#fff}\n.dsh-ig-gif-note-save{padding:0 16px;border:0;background:#fff;color:#1f2329;font-weight:600}\n.dsh-ig-gif-note-save:disabled{opacity:.45;cursor:default}\n.dsh-ig-gallery-gif-badge{position:absolute;left:10px;top:10px;padding:2px 7px;border-radius:6px;background:rgba(17,20,26,0.66);color:#fff;font-size:11px;font-weight:700;line-height:16px;letter-spacing:.3px;pointer-events:none;z-index:2;transition:opacity .15s ease}\n.dsh-ig-gallery-card:hover .dsh-ig-gallery-gif-badge,.dsh-ig-gallery-card.is-manage-mode .dsh-ig-gallery-gif-badge,.dsh-ig-gallery-card.is-selected .dsh-ig-gallery-gif-badge{opacity:0}\n.dsh-ig-gallery-gif-hover{position:absolute;inset:0;pointer-events:none}\n.dsh-ig-lightbox-img.dsh-ig-gallery-gif-img{max-height:calc(100vh - 400px)}\n.dsh-ig-dl-gallery .dsh-ig-dl-menu{top:auto;bottom:calc(100% + 8px);box-shadow:0 10px 30px rgba(0,0,0,0.4)}\n.dsh-ig-dl-gallery svg{width:14px;height:14px}\n";
@@ -1,5 +1,5 @@
1
1
  /** Modern DSH conversation node that keeps image artifacts outside Tool process folding. */
2
- import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
2
+ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
3
3
  export declare const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
4
4
  export interface ImageResultPresentation {
5
5
  readonly attachment: ImageAttachmentRef;
@@ -13,6 +13,12 @@ export interface ImageResultPresentation {
13
13
  readonly seed?: number;
14
14
  /** Attachment ids of the edit sources when this image came from edit_image; used to rebuild canvas edit chains. */
15
15
  readonly sourceAttachmentIds?: readonly string[];
16
+ /** Raw bytes in the vault when the attachment is a normalized copy; today only an animated GIF. */
17
+ readonly original?: {
18
+ readonly key: string;
19
+ readonly bytes: number;
20
+ readonly mediaType: ImageMediaType;
21
+ };
16
22
  }
17
23
  interface ImageResultState {
18
24
  /** Owning conversation turn; PTC (run_code) sub-call results carry no turn. */
@@ -72,4 +78,16 @@ export declare function imageResultsFromMeta(value: unknown): readonly ImageResu
72
78
  export declare function imageResultFromPtcDispatch(event: EventLike): ImageResultPresentation | undefined;
73
79
  /** Pair each rendered image with its own summary, including batches with failed items. */
74
80
  export declare function imageResultsFromPtcDispatch(event: EventLike): readonly ImageResultPresentation[];
81
+ /**
82
+ * The identity of one image version. A still is its attachment; an animated
83
+ * result is also its vault original, because the host normalizes a GIF to a
84
+ * still of its first frame and two animations that differ only after that
85
+ * frame share one attachment. Keying versions, gallery records and frame notes
86
+ * by the attachment alone would merge such animations into one.
87
+ */
88
+ export declare function imageVersionKey(attachmentId: string, original: {
89
+ readonly key: string;
90
+ } | undefined): string;
91
+ /** Parse a vault reference; anything malformed drops the reference, never the image that carries it. */
92
+ export declare function originalFromMeta(value: unknown): ImageResultPresentation['original'];
75
93
  export {};
@@ -0,0 +1,26 @@
1
+ import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import { type ImageResultPresentation } from './image-result-node.js';
3
+ import { type LocaleService } from './gallery-view.js';
4
+ export interface ImageCardFace {
5
+ locale?: LocaleService | undefined;
6
+ promoted: boolean;
7
+ }
8
+ export type ImageCardProps = PropsRuntime<'tool.call.toolview'> & InjectFace<ImageCardFace> & {
9
+ sessionId?: string | undefined;
10
+ };
11
+ export interface ImageResultNodeProps {
12
+ node: {
13
+ data: {
14
+ results: readonly ImageResultPresentation[];
15
+ };
16
+ };
17
+ locale?: LocaleService | undefined;
18
+ sessionId?: string | undefined;
19
+ /** The host's file opener, the one reply file links use; hosts that do not pass it get the plain path. */
20
+ openFile?: ((path: string) => void) | undefined;
21
+ }
22
+ export declare function GeneratedImageCard(props: ImageCardProps): import("react").JSX.Element;
23
+ /** Suppress the folded duplicate on modern hosts; render every image on legacy hosts. */
24
+ export declare function GeneratedImagesCard(props: ImageCardProps): import("react").JSX.Element;
25
+ /** Render modern image artifacts as final conversation output instead of Tool process content. */
26
+ export declare function PromotedImageResultNode(props: ImageResultNodeProps): import("react").JSX.Element;
@@ -11,7 +11,7 @@ export interface SettingsScope<T> {
11
11
  import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
12
12
  import { type ComfyUIWorkflowEntry, type OpenAICompatChannel, type ImageProvider } from '../shared.js';
13
13
  import { type LocaleService } from './gallery-view.js';
14
- import { type ImageResultPresentation } from './image-result-node.js';
14
+ export { GeneratedImageCard, GeneratedImagesCard, PromotedImageResultNode } from './image-result-view.js';
15
15
  type Provider = ImageProvider;
16
16
  interface ImageSettings {
17
17
  provider?: Provider;
@@ -95,20 +95,7 @@ interface SettingsFace {
95
95
  locale?: LocaleService | undefined;
96
96
  credentialEvents?: CredentialEvents | undefined;
97
97
  }
98
- interface ImageCardFace {
99
- locale?: LocaleService | undefined;
100
- promoted: boolean;
101
- }
102
98
  type SettingsCardProps = PropsRuntime<'settings.plugins.tab'> & InjectFace<SettingsFace>;
103
- type ImageCardProps = PropsRuntime<'tool.call.toolview'> & InjectFace<ImageCardFace>;
104
- interface ImageResultNodeProps {
105
- node: {
106
- data: {
107
- results: readonly ImageResultPresentation[];
108
- };
109
- };
110
- locale?: LocaleService | undefined;
111
- }
112
99
  /**
113
100
  * Required browser services. The settings form reader is deliberately absent:
114
101
  * DSH 0.1.7 serves it as `configForms` (keyed by the Loader entry id) while
@@ -121,9 +108,3 @@ export declare const inject: string[];
121
108
  export declare function apply(ctx: Context): void;
122
109
  /** Edit each provider independently, pick an explicit default, and verify keys inline. */
123
110
  export declare function ImageGenerationSettingsCard(props: SettingsCardProps): import("react").JSX.Element;
124
- /** Keep the legacy Tool row for old DSH and hand modern results to the independent Chat node. */
125
- export declare function GeneratedImageCard(props: ImageCardProps): import("react").JSX.Element;
126
- /** Suppress the folded duplicate on modern hosts; render every image on legacy hosts. */
127
- export declare function GeneratedImagesCard(props: ImageCardProps): import("react").JSX.Element;
128
- /** Render modern image artifacts as final conversation output instead of Tool process content. */
129
- export declare function PromotedImageResultNode(props: ImageResultNodeProps): import("react").JSX.Element;