@meiyukichan/dsh-sandore 0.1.0-rc.6
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.
- package/LICENSE +201 -0
- package/cordis.patch.yml +39 -0
- package/dsh.plugin.json +16 -0
- package/lib/client/CommandEditor.d.ts +19 -0
- package/lib/client/DagEditor.d.ts +22 -0
- package/lib/client/DefinitionDetail.d.ts +13 -0
- package/lib/client/DefinitionList.d.ts +19 -0
- package/lib/client/DiscussionInput.d.ts +63 -0
- package/lib/client/DragHandle.d.ts +16 -0
- package/lib/client/EditorPane.d.ts +13 -0
- package/lib/client/FlowEditor.d.ts +31 -0
- package/lib/client/InstanceList.d.ts +24 -0
- package/lib/client/NewInstanceDialog.d.ts +19 -0
- package/lib/client/PreviewPanel.d.ts +13 -0
- package/lib/client/RunView.d.ts +79 -0
- package/lib/client/StepChatPanel.d.ts +34 -0
- package/lib/client/TreeNav.d.ts +27 -0
- package/lib/client/UnifiedTimeline.d.ts +11 -0
- package/lib/client/WorkflowButton.d.ts +11 -0
- package/lib/client/WorkflowList.d.ts +18 -0
- package/lib/client/WorkflowOverlay.d.ts +6 -0
- package/lib/client/chat/AssistantMarkdown.d.ts +22 -0
- package/lib/client/chat/CompactionItem.d.ts +24 -0
- package/lib/client/chat/ContextBody.d.ts +136 -0
- package/lib/client/chat/ContextInjectionRow.d.ts +25 -0
- package/lib/client/chat/GenericCommandCard.d.ts +10 -0
- package/lib/client/chat/MessageIconActions.d.ts +41 -0
- package/lib/client/chat/MessageItem.d.ts +65 -0
- package/lib/client/chat/ReasoningRow.d.ts +13 -0
- package/lib/client/chat/TurnTailNodeView.d.ts +29 -0
- package/lib/client/chat/UserMessageRow.d.ts +18 -0
- package/lib/client/chat/attachment/ImageLightbox.d.ts +26 -0
- package/lib/client/chat/attachment/MessageImage.d.ts +56 -0
- package/lib/client/chat/attachment/image-attachment.d.ts +41 -0
- package/lib/client/chat/attachment/image-labels.d.ts +21 -0
- package/lib/client/chat/message-chrome.d.ts +51 -0
- package/lib/client/chat/reference/ReferenceIcon.d.ts +20 -0
- package/lib/client/chat/turn-assistant.d.ts +12 -0
- package/lib/client/chat/turn-metrics.d.ts +42 -0
- package/lib/client/chat/use-calendar-day.d.ts +5 -0
- package/lib/client/chat/use-throttled-visual-update.d.ts +7 -0
- package/lib/client/chat-flow.d.ts +110 -0
- package/lib/client/composer-selects.d.ts +17 -0
- package/lib/client/dag-editor-model.d.ts +141 -0
- package/lib/client/describe-error.d.ts +9 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/locales.d.ts +777 -0
- package/lib/client/model-faces.d.ts +156 -0
- package/lib/client/open-discussion.d.ts +24 -0
- package/lib/client/panel-model.d.ts +34 -0
- package/lib/client/remote.d.ts +34 -0
- package/lib/client/run-view-model.d.ts +304 -0
- package/lib/client/sandore-save.d.ts +41 -0
- package/lib/client/sandore-sse.d.ts +15 -0
- package/lib/client/step-chat-retry.d.ts +44 -0
- package/lib/client/tool/GenericToolCard.d.ts +28 -0
- package/lib/client/tool/ToolRow.d.ts +95 -0
- package/lib/client/tool/diff-card-model.d.ts +63 -0
- package/lib/client/tool/read-card-model.d.ts +67 -0
- package/lib/client/tool/search-card-model.d.ts +94 -0
- package/lib/client/tool/terminal-card-model.d.ts +86 -0
- package/lib/client/tool/tool-call-model.d.ts +74 -0
- package/lib/client/tool/web-card-model.d.ts +44 -0
- package/lib/client/use-focus-refresh.d.ts +1 -0
- package/lib/client/workflow-face.d.ts +68 -0
- package/lib/client/workflow-store.d.ts +80 -0
- package/lib/client/workspace-path.d.ts +15 -0
- package/lib/client.js +38389 -0
- package/lib/index.js +8854 -0
- package/lib/microflow/branch-evaluator.d.ts +290 -0
- package/lib/microflow/concurrency-limiter.d.ts +39 -0
- package/lib/microflow/errors.d.ts +39 -0
- package/lib/microflow/file-utils.d.ts +210 -0
- package/lib/microflow/finalize-manager.d.ts +414 -0
- package/lib/microflow/fork-session.d.ts +151 -0
- package/lib/microflow/index.d.ts +17 -0
- package/lib/microflow/internal.d.ts +56 -0
- package/lib/microflow/loop-manager.d.ts +127 -0
- package/lib/microflow/rules-resolver.d.ts +28 -0
- package/lib/microflow/stage-executor.d.ts +362 -0
- package/lib/microflow/step-navigator.d.ts +108 -0
- package/lib/microflow/step-runner.d.ts +190 -0
- package/lib/microflow/subagents.d.ts +380 -0
- package/lib/tool/index.d.ts +65 -0
- package/lib/typert.host.d.ts +16 -0
- package/lib/typert.host.js +25 -0
- package/lib/typert.remote-client.d.ts +114 -0
- package/lib/typert.remote-client.js +399 -0
- package/lib/workflow/ask-tool.d.ts +78 -0
- package/lib/workflow/dag-navigator.d.ts +136 -0
- package/lib/workflow/domain.d.ts +304 -0
- package/lib/workflow/engine-surface.d.ts +70 -0
- package/lib/workflow/errors.d.ts +13 -0
- package/lib/workflow/host-agent.d.ts +53 -0
- package/lib/workflow/index.d.ts +23 -0
- package/lib/workflow/interaction-manager.d.ts +170 -0
- package/lib/workflow/lifecycle-manager.d.ts +111 -0
- package/lib/workflow/persistence.d.ts +35 -0
- package/lib/workflow/plugin.d.ts +27 -0
- package/lib/workflow/reactive-state.d.ts +27 -0
- package/lib/workflow/recovery-manager.d.ts +158 -0
- package/lib/workflow/remote.d.ts +139 -0
- package/lib/workflow/schema.d.ts +1917 -0
- package/lib/workflow/seam.d.ts +254 -0
- package/lib/workflow/shell.d.ts +127 -0
- package/lib/workflow/sse.d.ts +16 -0
- package/lib/workflow/typert/typert.host.d.ts +16 -0
- package/lib/workflow/typert/typert.remote-client.d.ts +114 -0
- package/lib/workflow/types.d.ts +187 -0
- package/lib/workflow/wire.d.ts +256 -0
- package/lib/workflow/workflow-engine.d.ts +373 -0
- package/lib/workflow/yaml-error.d.ts +22 -0
- package/lib/workflow/yaml-parser.d.ts +23 -0
- package/package.json +140 -0
- package/packages/client/CommandEditor.tsx +73 -0
- package/packages/client/DagEditor.module.css +711 -0
- package/packages/client/DagEditor.tsx +1203 -0
- package/packages/client/DefinitionDetail.module.css +110 -0
- package/packages/client/DefinitionDetail.tsx +126 -0
- package/packages/client/DefinitionList.tsx +173 -0
- package/packages/client/DiscussionInput.tsx +763 -0
- package/packages/client/DragHandle.tsx +92 -0
- package/packages/client/EditorPane.tsx +70 -0
- package/packages/client/FlowEditor.tsx +227 -0
- package/packages/client/InstanceList.tsx +250 -0
- package/packages/client/NewInstanceDialog.module.css +244 -0
- package/packages/client/NewInstanceDialog.tsx +478 -0
- package/packages/client/PreviewPanel.tsx +55 -0
- package/packages/client/RunView.module.css +246 -0
- package/packages/client/RunView.tsx +314 -0
- package/packages/client/StepChatPanel.tsx +486 -0
- package/packages/client/TreeNav.tsx +294 -0
- package/packages/client/UnifiedTimeline.tsx +484 -0
- package/packages/client/WorkflowButton.module.css +72 -0
- package/packages/client/WorkflowButton.tsx +50 -0
- package/packages/client/WorkflowList.tsx +158 -0
- package/packages/client/WorkflowOverlay.module.css +390 -0
- package/packages/client/WorkflowOverlay.tsx +136 -0
- package/packages/client/WorkflowPanel.module.css +247 -0
- package/packages/client/chat/AssistantMarkdown.module.css +38 -0
- package/packages/client/chat/AssistantMarkdown.tsx +120 -0
- package/packages/client/chat/ChatView.module.css +198 -0
- package/packages/client/chat/CompactionItem.tsx +85 -0
- package/packages/client/chat/ContextBody.module.css +162 -0
- package/packages/client/chat/ContextBody.tsx +598 -0
- package/packages/client/chat/ContextInjectionRow.module.css +63 -0
- package/packages/client/chat/ContextInjectionRow.tsx +78 -0
- package/packages/client/chat/GenericCommandCard.module.css +87 -0
- package/packages/client/chat/GenericCommandCard.tsx +76 -0
- package/packages/client/chat/MessageIconActions.module.css +84 -0
- package/packages/client/chat/MessageIconActions.tsx +147 -0
- package/packages/client/chat/MessageItem.module.css +293 -0
- package/packages/client/chat/MessageItem.tsx +384 -0
- package/packages/client/chat/ReasoningRow.module.css +82 -0
- package/packages/client/chat/ReasoningRow.tsx +70 -0
- package/packages/client/chat/TurnTailNodeView.module.css +10 -0
- package/packages/client/chat/TurnTailNodeView.tsx +63 -0
- package/packages/client/chat/UserMessageRow.module.css +75 -0
- package/packages/client/chat/UserMessageRow.tsx +92 -0
- package/packages/client/chat/accessibility.module.css +9 -0
- package/packages/client/chat/attachment/ImageLightbox.module.css +45 -0
- package/packages/client/chat/attachment/ImageLightbox.tsx +70 -0
- package/packages/client/chat/attachment/MessageImage.module.css +70 -0
- package/packages/client/chat/attachment/MessageImage.tsx +136 -0
- package/packages/client/chat/attachment/image-attachment.ts +98 -0
- package/packages/client/chat/attachment/image-labels.ts +34 -0
- package/packages/client/chat/message-chrome.ts +96 -0
- package/packages/client/chat/reference/ReferenceIcon.tsx +40 -0
- package/packages/client/chat/turn-assistant.ts +15 -0
- package/packages/client/chat/turn-metrics.ts +102 -0
- package/packages/client/chat/use-calendar-day.ts +27 -0
- package/packages/client/chat/use-throttled-visual-update.ts +44 -0
- package/packages/client/chat-flow.spec.ts +357 -0
- package/packages/client/chat-flow.ts +278 -0
- package/packages/client/composer-selects.module.css +376 -0
- package/packages/client/composer-selects.tsx +531 -0
- package/packages/client/dag-editor-model.spec.ts +173 -0
- package/packages/client/dag-editor-model.ts +359 -0
- package/packages/client/describe-error.spec.ts +18 -0
- package/packages/client/describe-error.ts +12 -0
- package/packages/client/index.ts +144 -0
- package/packages/client/input.module.css +854 -0
- package/packages/client/is-conditional-check.spec.ts +34 -0
- package/packages/client/locales.ts +789 -0
- package/packages/client/model-faces.ts +225 -0
- package/packages/client/open-discussion.spec.ts +39 -0
- package/packages/client/open-discussion.ts +32 -0
- package/packages/client/panel-model.spec.ts +152 -0
- package/packages/client/panel-model.ts +72 -0
- package/packages/client/preview.module.css +95 -0
- package/packages/client/react-dom.d.ts +11 -0
- package/packages/client/reactflow-theme.css +91 -0
- package/packages/client/remote.spec.ts +41 -0
- package/packages/client/remote.ts +46 -0
- package/packages/client/run-view-model.spec.ts +641 -0
- package/packages/client/run-view-model.ts +636 -0
- package/packages/client/sandore-save.spec.ts +146 -0
- package/packages/client/sandore-save.ts +97 -0
- package/packages/client/sandore-sse.ts +59 -0
- package/packages/client/step-chat-retry.spec.ts +70 -0
- package/packages/client/step-chat-retry.ts +65 -0
- package/packages/client/timeline.module.css +503 -0
- package/packages/client/tool/GenericToolCard.tsx +99 -0
- package/packages/client/tool/ToolRow.module.css +308 -0
- package/packages/client/tool/ToolRow.tsx +328 -0
- package/packages/client/tool/diff-card-model.ts +103 -0
- package/packages/client/tool/read-card-model.ts +86 -0
- package/packages/client/tool/search-card-model.ts +166 -0
- package/packages/client/tool/terminal-card-model.ts +225 -0
- package/packages/client/tool/tool-call-model.ts +267 -0
- package/packages/client/tool/web-card-model.ts +80 -0
- package/packages/client/tree-nav.module.css +282 -0
- package/packages/client/use-focus-refresh.ts +23 -0
- package/packages/client/workflow-face.ts +72 -0
- package/packages/client/workflow-store.spec.ts +49 -0
- package/packages/client/workflow-store.ts +161 -0
- package/packages/client/workspace-path.ts +27 -0
- package/packages/css-modules.d.ts +6 -0
- package/packages/microflow/branch-evaluator.spec.ts +745 -0
- package/packages/microflow/branch-evaluator.ts +1037 -0
- package/packages/microflow/concurrency-limiter.ts +70 -0
- package/packages/microflow/errors.ts +64 -0
- package/packages/microflow/file-utils.spec.ts +604 -0
- package/packages/microflow/file-utils.ts +521 -0
- package/packages/microflow/finalize-manager.spec.ts +1977 -0
- package/packages/microflow/finalize-manager.ts +1464 -0
- package/packages/microflow/fork-session.spec.ts +184 -0
- package/packages/microflow/fork-session.ts +316 -0
- package/packages/microflow/index.ts +17 -0
- package/packages/microflow/internal.ts +166 -0
- package/packages/microflow/loop-manager.spec.ts +294 -0
- package/packages/microflow/loop-manager.ts +255 -0
- package/packages/microflow/rules-resolver.spec.ts +147 -0
- package/packages/microflow/rules-resolver.ts +58 -0
- package/packages/microflow/stage-executor.spec.ts +1577 -0
- package/packages/microflow/stage-executor.ts +1478 -0
- package/packages/microflow/step-navigator.spec.ts +301 -0
- package/packages/microflow/step-navigator.ts +300 -0
- package/packages/microflow/step-runner.spec.ts +1132 -0
- package/packages/microflow/step-runner.ts +752 -0
- package/packages/microflow/subagents.spec.ts +215 -0
- package/packages/microflow/subagents.ts +422 -0
- package/packages/tests/fixtures/test-workflow.yaml +128 -0
- package/packages/tests/mocks/agents.ts +206 -0
- package/packages/tests/mocks/llm.ts +46 -0
- package/packages/tests/mocks/session-title.ts +31 -0
- package/packages/tests/mocks/sessions.ts +131 -0
- package/packages/tests/mocks/shell.ts +60 -0
- package/packages/tests/mocks/storage-domain.ts +101 -0
- package/packages/tests/mocks/subagents.ts +197 -0
- package/packages/tests/mocks/subprocess.ts +59 -0
- package/packages/tests/mocks/user-questions.ts +33 -0
- package/packages/tests/mocks/workspace.ts +52 -0
- package/packages/tool/index.spec.ts +287 -0
- package/packages/tool/index.ts +348 -0
- package/packages/workflow/ask-tool.ts +173 -0
- package/packages/workflow/dag-navigator.spec.ts +236 -0
- package/packages/workflow/dag-navigator.ts +425 -0
- package/packages/workflow/domain.ts +26 -0
- package/packages/workflow/engine-surface.ts +71 -0
- package/packages/workflow/errors.ts +13 -0
- package/packages/workflow/host-agent.spec.ts +92 -0
- package/packages/workflow/host-agent.ts +62 -0
- package/packages/workflow/index.ts +25 -0
- package/packages/workflow/interaction-manager.spec.ts +390 -0
- package/packages/workflow/interaction-manager.ts +397 -0
- package/packages/workflow/lifecycle-manager.spec.ts +683 -0
- package/packages/workflow/lifecycle-manager.ts +288 -0
- package/packages/workflow/persistence.spec.ts +109 -0
- package/packages/workflow/persistence.ts +64 -0
- package/packages/workflow/plugin.ts +39 -0
- package/packages/workflow/reactive-state.spec.ts +69 -0
- package/packages/workflow/reactive-state.ts +52 -0
- package/packages/workflow/recovery-manager.spec.ts +606 -0
- package/packages/workflow/recovery-manager.ts +471 -0
- package/packages/workflow/remote.spec.ts +1045 -0
- package/packages/workflow/remote.ts +479 -0
- package/packages/workflow/schema.spec.ts +303 -0
- package/packages/workflow/schema.ts +562 -0
- package/packages/workflow/seam.ts +293 -0
- package/packages/workflow/shell.ts +142 -0
- package/packages/workflow/sse.ts +110 -0
- package/packages/workflow/typert/typert.host.d.ts +16 -0
- package/packages/workflow/typert/typert.host.js +25 -0
- package/packages/workflow/typert/typert.remote-client.d.ts +114 -0
- package/packages/workflow/typert/typert.remote-client.js +399 -0
- package/packages/workflow/types.ts +258 -0
- package/packages/workflow/wire.ts +267 -0
- package/packages/workflow/workflow-engine-e2e.spec.ts +262 -0
- package/packages/workflow/workflow-engine-real-e2e.spec.ts +922 -0
- package/packages/workflow/workflow-engine.spec.ts +4389 -0
- package/packages/workflow/workflow-engine.ts +2133 -0
- package/packages/workflow/yaml-error.ts +29 -0
- package/packages/workflow/yaml-parser.spec.ts +757 -0
- package/packages/workflow/yaml-parser.ts +287 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageImage:抄自 DSH `packages/client/ui-attachment/src/MessageImage.tsx`。
|
|
3
|
+
* 历史消息图片的紧凑渲染(可重试加载 + 点击打开原图预览;单图按 long-edge 240px 定界,
|
|
4
|
+
* 多图渲染为 64px 方块瓦片)。attachment 类型经本地结构复刻(dsh-attachment 非依赖),
|
|
5
|
+
* labels 面收窄为 sandore 命名空间,其余逐字保留。
|
|
6
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/attachment/MessageImage
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
9
|
+
import type { ReactNode } from 'react'
|
|
10
|
+
import { ImageLightbox } from './ImageLightbox.tsx'
|
|
11
|
+
import type { ImageLightboxLabels } from './ImageLightbox.tsx'
|
|
12
|
+
import type { SandoreImageAttachmentRef } from './image-attachment.ts'
|
|
13
|
+
import css from './MessageImage.module.css'
|
|
14
|
+
|
|
15
|
+
/** Loads a session-authorized durable image URL. */
|
|
16
|
+
export type ImageLoader = (attachment: SandoreImageAttachmentRef) => Promise<string>
|
|
17
|
+
|
|
18
|
+
/** 历史图片组渲染器(对齐 DSH slots.ts 的 `RenderMessageImages`,attachment 面本地复刻)。 */
|
|
19
|
+
export type SandoreRenderMessageImages = (owner: {
|
|
20
|
+
/** Consecutive image blocks rendered as one gallery. */
|
|
21
|
+
images: readonly { readonly attachment: SandoreImageAttachmentRef }[]
|
|
22
|
+
/** Message-side alignment. */
|
|
23
|
+
align: 'start' | 'end'
|
|
24
|
+
}) => ReactNode
|
|
25
|
+
|
|
26
|
+
/** Message-image strings the owner resolves from its own locale namespace. */
|
|
27
|
+
export interface MessageImageLabels {
|
|
28
|
+
/** Fallback display name for an unnamed image. */
|
|
29
|
+
image: string
|
|
30
|
+
/** Thumbnail tooltip inviting the original-image preview. */
|
|
31
|
+
open: string
|
|
32
|
+
/** Accessible thumbnail label; receives the image's display name. */
|
|
33
|
+
openNamed: (label: string) => string
|
|
34
|
+
/** Loading placeholder shown until bytes resolve. */
|
|
35
|
+
loading: string
|
|
36
|
+
/** Retry-control label shown when the load fails. */
|
|
37
|
+
loadFailed: string
|
|
38
|
+
/** Lightbox strings forwarded to the opened preview. */
|
|
39
|
+
lightbox: ImageLightboxLabels
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Display box for a lone image (DeepSeek Chat rule): long edge 240px with
|
|
43
|
+
* the rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
|
|
44
|
+
* `object-fit: cover` — and never upscaled past the image's natural size. The
|
|
45
|
+
* crop anchor keeps the top of very tall images and the left of very wide
|
|
46
|
+
* ones, where the informative content usually starts. */
|
|
47
|
+
function singleFit(attachment: SandoreImageAttachmentRef): { width: number; height: number; objectPosition: string } {
|
|
48
|
+
const natural = attachment.width / attachment.height
|
|
49
|
+
const ratio = Math.min(4, Math.max(0.25, natural))
|
|
50
|
+
const box = ratio >= 1 ? { width: 240, height: 240 / ratio } : { width: 240 * ratio, height: 240 }
|
|
51
|
+
const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height)
|
|
52
|
+
return {
|
|
53
|
+
width: Math.max(1, Math.round(box.width * scale)),
|
|
54
|
+
height: Math.max(1, Math.round(box.height * scale)),
|
|
55
|
+
objectPosition: natural < 0.25 ? 'center top' : natural > 4 ? 'left center' : 'center',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compact history renderer with retryable loading and click-to-open original
|
|
61
|
+
* preview. A lone image renders at its `singleFit` size; an image among
|
|
62
|
+
* several renders as a fixed 64px square tile.
|
|
63
|
+
*
|
|
64
|
+
* @param props.attachment - the durable image reference to load and bound.
|
|
65
|
+
* @param props.load - session-authorized URL loader.
|
|
66
|
+
* @param props.variant - `single` for a message's lone image, `tile` otherwise.
|
|
67
|
+
* @param props.labels - resolved strings (tooltip, loading, retry, lightbox).
|
|
68
|
+
* @returns the bounded thumbnail button, or the retry control on failure.
|
|
69
|
+
*/
|
|
70
|
+
export function MessageImage({ attachment, load, variant, labels }: {
|
|
71
|
+
attachment: SandoreImageAttachmentRef
|
|
72
|
+
load: ImageLoader
|
|
73
|
+
variant: 'single' | 'tile'
|
|
74
|
+
labels: MessageImageLabels
|
|
75
|
+
}) {
|
|
76
|
+
const [src, setSrc] = useState<string | null>(null)
|
|
77
|
+
const [error, setError] = useState(false)
|
|
78
|
+
const [open, setOpen] = useState(false)
|
|
79
|
+
// Retry re-arms the one load effect below, so every attempt — first load or
|
|
80
|
+
// retry — runs under the same liveness guard and the same reset.
|
|
81
|
+
const [attempt, setAttempt] = useState(0)
|
|
82
|
+
const request = useCallback(() => { setAttempt(a => a + 1) }, [])
|
|
83
|
+
const close = useCallback(() => { setOpen(false) }, [])
|
|
84
|
+
const fit = useMemo(
|
|
85
|
+
() => (variant === 'single' ? singleFit(attachment) : undefined),
|
|
86
|
+
[attachment, variant],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
let live = true
|
|
91
|
+
setError(false)
|
|
92
|
+
setSrc(null)
|
|
93
|
+
void load(attachment).then((url) => { if (live) setSrc(url) }).catch(() => { if (live) setError(true) })
|
|
94
|
+
return () => { live = false }
|
|
95
|
+
}, [attachment, load, attempt])
|
|
96
|
+
|
|
97
|
+
const label = attachment.name ?? labels.image
|
|
98
|
+
if (error) return <button type="button" className={css.error} data-variant={variant} onClick={request}>{labels.loadFailed}</button>
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className={css.frame}
|
|
104
|
+
data-variant={variant}
|
|
105
|
+
style={fit === undefined ? undefined : { width: fit.width, height: fit.height }}
|
|
106
|
+
title={labels.open}
|
|
107
|
+
aria-label={labels.openNamed(label)}
|
|
108
|
+
onClick={() => { if (src !== null) setOpen(true) }}
|
|
109
|
+
>
|
|
110
|
+
{src === null
|
|
111
|
+
? <span className={css.loading}>{labels.loading}</span>
|
|
112
|
+
: <img src={src} alt={label} style={fit === undefined ? undefined : { objectPosition: fit.objectPosition }} />}
|
|
113
|
+
</button>
|
|
114
|
+
{open && src !== null && <ImageLightbox src={src} alt={label} labels={labels.lightbox} onClose={close} />}
|
|
115
|
+
</>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Wrapping image group shared by user and assistant history: a lone image
|
|
120
|
+
* renders large, several render as 64px square tiles (DeepSeek Chat rule). */
|
|
121
|
+
export function ImageGallery({ images, load, align, labels }: {
|
|
122
|
+
images: readonly { attachment: SandoreImageAttachmentRef }[]
|
|
123
|
+
load: ImageLoader
|
|
124
|
+
align: 'start' | 'end'
|
|
125
|
+
labels: MessageImageLabels
|
|
126
|
+
}) {
|
|
127
|
+
if (images.length === 0) return null
|
|
128
|
+
const variant = images.length === 1 ? 'single' : 'tile'
|
|
129
|
+
return (
|
|
130
|
+
<div className={css.gallery} data-align={align}>
|
|
131
|
+
{images.map((image, index) => (
|
|
132
|
+
<MessageImage key={`${image.attachment.attachmentId}:${index}`} {...image} load={load} variant={variant} labels={labels} />
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-attachment:沙盒面板的本地图片附件面。
|
|
3
|
+
*
|
|
4
|
+
* DSH 的 `ImageAttachmentRef` 定义在 `@deepseek-ai/dsh-attachment`(runtime 的
|
|
5
|
+
* `ISession.readAttachment` 返回它),sandore 面板不声明该依赖,故按
|
|
6
|
+
* `packages/attachment/attachment/src/types.ts` 的字段结构本地复刻(仅类型,无行为)。
|
|
7
|
+
* `resolveImage` 抄自 DSH `ui-conversation/src/client/service.ts` 的
|
|
8
|
+
* `ConversationController.resolveImage`:经 `session.readAttachment` 取回字节,构建
|
|
9
|
+
* blob URL(不可用时退化为 data URL),按 session+attachmentId 缓存。
|
|
10
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/attachment/image-attachment
|
|
11
|
+
*/
|
|
12
|
+
import type { SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
13
|
+
|
|
14
|
+
/** Raster image formats accepted by the version-one attachment path. */
|
|
15
|
+
export type SandoreImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
|
|
16
|
+
|
|
17
|
+
/** Durable, serializable metadata for one immutable image object(dsh-attachment 的 ImageAttachmentRef 结构复刻)。 */
|
|
18
|
+
export interface SandoreImageAttachmentRef {
|
|
19
|
+
/** Opaque storage identifier; never a filesystem path or bearer URL. */
|
|
20
|
+
attachmentId: string
|
|
21
|
+
/** Media type verified from the stored bytes. */
|
|
22
|
+
mediaType: SandoreImageMediaType
|
|
23
|
+
/** Exact encoded byte length. */
|
|
24
|
+
bytes: number
|
|
25
|
+
/** Intrinsic encoded width in pixels. */
|
|
26
|
+
width: number
|
|
27
|
+
/** Intrinsic encoded height in pixels. */
|
|
28
|
+
height: number
|
|
29
|
+
/** Optional display name stripped of local path information. */
|
|
30
|
+
name?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
34
|
+
let binary = ''
|
|
35
|
+
const chunk = 0x8000
|
|
36
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
37
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk))
|
|
38
|
+
}
|
|
39
|
+
return btoa(binary)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PendingImage {
|
|
43
|
+
readonly generation: number
|
|
44
|
+
readonly pending: Promise<string>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 构建一个会话授权的历史图片 URL 加载器(`(attachment) => Promise<string>`)。
|
|
49
|
+
* 行为与 DSH `ConversationController.resolveImage` 一致:按 session+attachmentId 缓存,
|
|
50
|
+
* 取回字节后优先 blob URL(`URL.createObjectURL` 不可用时退化为 base64 data URL)。
|
|
51
|
+
* 调用方应在卸载时 `dispose()` 释放创建的 blob URL。
|
|
52
|
+
* @param session - 所属会话面(readAttachment 授权源)。
|
|
53
|
+
* @returns `{ load, dispose }`:load 为加载器;dispose 释放缓存与已创建 URL。
|
|
54
|
+
*/
|
|
55
|
+
export function createImageLoader(session: SessionFace): {
|
|
56
|
+
load(attachment: SandoreImageAttachmentRef): Promise<string>
|
|
57
|
+
dispose(): void
|
|
58
|
+
} {
|
|
59
|
+
const sessionId = session.sessionId as SessionId
|
|
60
|
+
const generation = 0
|
|
61
|
+
const imageUrls = new Map<string, PendingImage>()
|
|
62
|
+
const createdImageUrls = new Set<string>()
|
|
63
|
+
const revokePreview = (url: string): void => {
|
|
64
|
+
createdImageUrls.delete(url)
|
|
65
|
+
if (typeof URL.revokeObjectURL === 'function') URL.revokeObjectURL(url)
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
load(attachment: SandoreImageAttachmentRef): Promise<string> {
|
|
69
|
+
const key = `${sessionId}:${attachment.attachmentId}`
|
|
70
|
+
const cached = imageUrls.get(key)
|
|
71
|
+
if (cached !== undefined) return cached.pending
|
|
72
|
+
// readAttachment 的参数为 runtime 的 branded AttachmentId(dsh-api-remotes);
|
|
73
|
+
// 本地结构复刻的 attachmentId 为 string,经参数类型转换(值本身不变)。
|
|
74
|
+
const attachmentId = attachment.attachmentId as unknown as Parameters<SessionFace['readAttachment']>[0]
|
|
75
|
+
const pending = session.readAttachment(attachmentId)
|
|
76
|
+
.then((result) => {
|
|
77
|
+
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`)
|
|
78
|
+
if (typeof URL.createObjectURL !== 'function') {
|
|
79
|
+
return `data:${attachment.mediaType};base64,${bytesToBase64(Uint8Array.from(result.value.data))}`
|
|
80
|
+
}
|
|
81
|
+
const bytes = Uint8Array.from(result.value.data)
|
|
82
|
+
const url = URL.createObjectURL(new Blob([bytes.buffer], { type: attachment.mediaType }))
|
|
83
|
+
createdImageUrls.add(url)
|
|
84
|
+
return url
|
|
85
|
+
})
|
|
86
|
+
.catch((error: unknown) => {
|
|
87
|
+
if (imageUrls.get(key)?.generation === generation) imageUrls.delete(key)
|
|
88
|
+
throw error
|
|
89
|
+
})
|
|
90
|
+
imageUrls.set(key, { generation, pending })
|
|
91
|
+
return pending
|
|
92
|
+
},
|
|
93
|
+
dispose(): void {
|
|
94
|
+
imageUrls.clear()
|
|
95
|
+
for (const url of [...createdImageUrls]) revokePreview(url)
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-labels:抄自 DSH `packages/client/ui-attachment/src/client/labels.ts`(消息图片
|
|
3
|
+
* 部分;drop/rail 标签不属于聊天面板)。把 sandore 命名空间的 image.* 键解析为
|
|
4
|
+
* MessageImage/ImageLightbox 的标签面。逐字保留逻辑。
|
|
5
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/attachment/image-labels
|
|
6
|
+
*/
|
|
7
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
|
+
import type { ImageLightboxLabels } from './ImageLightbox.tsx'
|
|
9
|
+
import type { MessageImageLabels } from './MessageImage.tsx'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve original-image lightbox strings from the sandore namespace.
|
|
13
|
+
* @param t - sandore namespace translator.
|
|
14
|
+
* @returns translated lightbox labels.
|
|
15
|
+
*/
|
|
16
|
+
export function lightboxLabels(t: TranslateNS<'sandore'>): ImageLightboxLabels {
|
|
17
|
+
return { dialog: t('image.preview'), close: t('image.closePreview') }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve historical message-image strings from the sandore namespace.
|
|
22
|
+
* @param t - sandore namespace translator.
|
|
23
|
+
* @returns translated message-image labels.
|
|
24
|
+
*/
|
|
25
|
+
export function messageImageLabels(t: TranslateNS<'sandore'>): MessageImageLabels {
|
|
26
|
+
return {
|
|
27
|
+
image: t('image.label'),
|
|
28
|
+
open: t('image.openOriginal'),
|
|
29
|
+
openNamed: label => t('image.openOriginalLabel', { label }),
|
|
30
|
+
loading: t('image.loading'),
|
|
31
|
+
loadFailed: t('image.loadFailed'),
|
|
32
|
+
lightbox: lightboxLabels(t),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message-chrome:抄自 DSH `packages/client/ui-conversation/src/client/chat/message-chrome.ts`。
|
|
3
|
+
* 用户/agent 消息 IconActions 行共用的时间标签辅助(本地日历日、运行时长、时钟)。
|
|
4
|
+
* 仅把 locale 面从 conversation 命名空间收窄为 sandore 命名空间(key 集抄进
|
|
5
|
+
* sandore 字典),逻辑逐字保留。
|
|
6
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/message-chrome
|
|
7
|
+
*/
|
|
8
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
9
|
+
|
|
10
|
+
function pad2(n: number): string {
|
|
11
|
+
return String(n).padStart(2, '0')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Local calendar-day epoch (ms at local midnight) for an instant.
|
|
16
|
+
* @param ms - Unix epoch ms.
|
|
17
|
+
* @returns Midnight of that local calendar day.
|
|
18
|
+
*/
|
|
19
|
+
export function startOfLocalDay(ms: number): number {
|
|
20
|
+
const d = new Date(ms)
|
|
21
|
+
d.setHours(0, 0, 0, 0)
|
|
22
|
+
return d.getTime()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Delay until the next local midnight after `ms` (at least 1ms).
|
|
27
|
+
* @param ms - Unix epoch ms.
|
|
28
|
+
* @returns Milliseconds until the following local midnight.
|
|
29
|
+
*/
|
|
30
|
+
export function msUntilNextLocalMidnight(ms: number): number {
|
|
31
|
+
const next = new Date(ms)
|
|
32
|
+
next.setHours(24, 0, 0, 0)
|
|
33
|
+
return Math.max(next.getTime() - ms, 1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Localized elapsed-time label shared by running and settled turn chrome.
|
|
38
|
+
* @param ms - Elapsed duration in milliseconds (negatives clamp to zero).
|
|
39
|
+
* @param t - Translate seat supplying the duration templates.
|
|
40
|
+
* @returns Display string in whole seconds.
|
|
41
|
+
*/
|
|
42
|
+
export function formatRunDuration(ms: number, t: TranslateNS<'sandore'>): string {
|
|
43
|
+
const total = Math.max(0, Math.floor(ms / 1000))
|
|
44
|
+
const minutes = Math.floor(total / 60)
|
|
45
|
+
const seconds = total % 60
|
|
46
|
+
return minutes > 0
|
|
47
|
+
? t('duration.minutes', { minutes, seconds: String(seconds).padStart(2, '0') })
|
|
48
|
+
: t('duration.seconds', { seconds })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sub-turn latency figure: one decimal under ten seconds, whole seconds
|
|
53
|
+
* beyond. Unit-less so the locale template owns the second suffix.
|
|
54
|
+
* @param ms - Latency in milliseconds (negatives clamp to zero).
|
|
55
|
+
* @returns Display number in seconds without unit.
|
|
56
|
+
*/
|
|
57
|
+
export function formatLatencySeconds(ms: number): string {
|
|
58
|
+
const s = Math.max(0, ms) / 1000
|
|
59
|
+
return s < 10 ? String(Math.round(s * 10) / 10) : String(Math.round(s))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decode-throughput figure: whole tokens from ten up, one decimal below.
|
|
64
|
+
* @param tps - Tokens per second.
|
|
65
|
+
* @returns Display number without unit.
|
|
66
|
+
*/
|
|
67
|
+
export function formatTokensPerSecond(tps: number): string {
|
|
68
|
+
const clamped = Math.max(0, tps)
|
|
69
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compact local timestamp for message IconActions. Same calendar day →
|
|
74
|
+
* `HH:mm`; earlier this year → the `clock.md` date template + clock; other
|
|
75
|
+
* years → the `clock.ymd` template + clock. Pure: the date templates arrive
|
|
76
|
+
* through the caller's locale seat.
|
|
77
|
+
* @param time - Unix epoch ms from the source session event.
|
|
78
|
+
* @param t - translate seat supplying the `clock.md` / `clock.ymd` templates.
|
|
79
|
+
* @param now - Reference instant for the day/year cut (defaults to wall clock).
|
|
80
|
+
* @returns Date-aware clock string (24-hour, zero-padded time).
|
|
81
|
+
*/
|
|
82
|
+
export function formatMessageClock(time: number, t: TranslateNS<'sandore'>, now: number = Date.now()): string {
|
|
83
|
+
const d = new Date(time)
|
|
84
|
+
const n = new Date(now)
|
|
85
|
+
const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`
|
|
86
|
+
if (
|
|
87
|
+
d.getFullYear() === n.getFullYear()
|
|
88
|
+
&& d.getMonth() === n.getMonth()
|
|
89
|
+
&& d.getDate() === n.getDate()
|
|
90
|
+
) {
|
|
91
|
+
return clock
|
|
92
|
+
}
|
|
93
|
+
const params = { y: d.getFullYear(), m: d.getMonth() + 1, d: d.getDate() }
|
|
94
|
+
const md = d.getFullYear() === n.getFullYear() ? t('clock.md', params) : t('clock.ymd', params)
|
|
95
|
+
return `${md} ${clock}`
|
|
96
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReferenceIcon:抄自 DSH `packages/client/ui-conversation/src/client/reference/ReferenceIcon.tsx`。
|
|
3
|
+
* 用户气泡内联引用(@会话/@文件/@文件夹)域字形。逐字保留。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/reference/ReferenceIcon
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
IconBrowseOutline16, IconFolderClose16,
|
|
9
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
10
|
+
|
|
11
|
+
/** Reference domains with distinct composer and transcript glyphs. */
|
|
12
|
+
export type ReferenceIconKind = 'session' | 'file' | 'folder'
|
|
13
|
+
|
|
14
|
+
/** Props shared by inline reference glyphs. */
|
|
15
|
+
export interface ReferenceIconProps {
|
|
16
|
+
kind: ReferenceIconKind
|
|
17
|
+
size?: number
|
|
18
|
+
className?: string | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render the icon that identifies one inline reference domain.
|
|
23
|
+
* @param props - Reference kind, optional size, and optional CSS class.
|
|
24
|
+
* @returns The corresponding current-color SVG glyph.
|
|
25
|
+
*/
|
|
26
|
+
export function ReferenceIcon({ kind, size = 16, className }: ReferenceIconProps): ReactNode {
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case 'session':
|
|
29
|
+
return (
|
|
30
|
+
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
31
|
+
<path
|
|
32
|
+
d="M8 0.597656C3.91296 0.597656 0.599716 3.91103 0.599609 7.99805C0.599609 9.13171 0.854567 10.2079 1.31152 11.1699L1.59277 11.7607L2.77441 11.1992L2.49414 10.6084L2.36035 10.3076C2.06865 9.59612 1.90723 8.81645 1.90723 7.99805C1.90733 4.63362 4.63554 1.90625 8 1.90625C11.3644 1.90635 14.0917 4.63368 14.0918 7.99805C14.0918 11.3625 11.3644 14.0907 8 14.0908C7.311 14.0908 6.80642 14.0414 6.35938 13.918C5.919 13.7963 5.50105 13.5929 5.00098 13.2441C4.26805 12.7329 3.21756 12.5526 2.35156 13.0996L2.33789 13.1084L2.32422 13.1182L1.74805 13.5234L2.18164 14.8184L3.05957 14.2002C3.37505 14.0068 3.84248 14.0319 4.25195 14.3174C4.84447 14.7307 5.39718 15.009 6.01172 15.1787C6.61963 15.3465 7.25579 15.3984 8 15.3984C12.087 15.3983 15.4004 12.0851 15.4004 7.99805C15.4003 3.9111 12.087 0.59776 8 0.597656ZM4.56836 8.50977V9.80371H8.12402V8.50977H4.56836ZM4.56836 7.30078H11.4619V6.00684H4.56836V7.30078Z"
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
case 'file': return <IconBrowseOutline16 size={size} className={className} />
|
|
38
|
+
case 'folder': return <IconFolderClose16 size={size} className={className} />
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-assistant:抄自 DSH `packages/client/ui-conversation/src/client/chat/turn-assistant.ts`。
|
|
3
|
+
* 收集一个 Assistant 生命周期中的可见散文(turn-tail 复制按钮取文本用)。逐字保留。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/turn-assistant
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect visible prose from one Assistant lifecycle.
|
|
10
|
+
* @param blocks - Assistant content blocks.
|
|
11
|
+
* @returns concatenated text blocks.
|
|
12
|
+
*/
|
|
13
|
+
export function assistantText(blocks: readonly AssistantBlock[]): string {
|
|
14
|
+
return blocks.flatMap(block => block.kind === 'text' ? [block.text] : []).join('')
|
|
15
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-metrics:抄自 DSH `packages/client/ui-conversation/src/client/chat/turn-metrics.ts`。
|
|
3
|
+
* 已结算 turn 尾部(tail)与 StatsLine 共用的延迟/吞吐折叠。逐字保留。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/turn-metrics
|
|
5
|
+
*/
|
|
6
|
+
// Latency/throughput folds shared by the settled turn footer and StatsLine.
|
|
7
|
+
|
|
8
|
+
import type { AssistantMessageNode, ConversationNode } from '@deepseek-ai/dsh-client-runtime/client'
|
|
9
|
+
|
|
10
|
+
/** Latency and decode-throughput readings for one turn's footer. */
|
|
11
|
+
export interface TurnMetrics {
|
|
12
|
+
/** First-step TTFT in ms; absent when that step carries no recorded timing. */
|
|
13
|
+
ttftMs?: number
|
|
14
|
+
/** Decode throughput over steps carrying both timing and provider usage. */
|
|
15
|
+
tokensPerSecond?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One assistant step's derivable latency facts; null marks an unrecorded part. */
|
|
19
|
+
export interface StepReading {
|
|
20
|
+
/** step/start → first token delta, in ms. */
|
|
21
|
+
ttftMs: number | null
|
|
22
|
+
/** First token delta → final message, in ms. */
|
|
23
|
+
decodeMs: number | null
|
|
24
|
+
/** Provider-reported completion tokens. */
|
|
25
|
+
outputTokens: number | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UsageLike {
|
|
29
|
+
outputTokens?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type AssistantNode = AssistantMessageNode
|
|
33
|
+
|
|
34
|
+
function usageOutputTokens(usage: unknown): number | null {
|
|
35
|
+
if (typeof usage !== 'object' || usage === null) return null
|
|
36
|
+
const value = (usage as UsageLike).outputTokens
|
|
37
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read one assistant node's TTFT, decode wall time, and output tokens.
|
|
42
|
+
* @param node - A settled assistant node.
|
|
43
|
+
* @returns Per-part readings with `null` for unrecorded values.
|
|
44
|
+
*/
|
|
45
|
+
export function assistantStepReading(node: AssistantNode): StepReading {
|
|
46
|
+
const timing = node.timing
|
|
47
|
+
const ttftMs = timing !== undefined && timing.stepStartTime !== null && timing.firstTokenTime !== null
|
|
48
|
+
? Math.max(0, timing.firstTokenTime - timing.stepStartTime)
|
|
49
|
+
: null
|
|
50
|
+
const decodeMs = timing !== undefined && timing.firstTokenTime !== null
|
|
51
|
+
? Math.max(0, timing.completedTime - timing.firstTokenTime)
|
|
52
|
+
: null
|
|
53
|
+
return { ttftMs, decodeMs, outputTokens: usageOutputTokens(node.usage) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TurnFold {
|
|
57
|
+
firstStep: number
|
|
58
|
+
firstStepTtftMs: number | null
|
|
59
|
+
decodeMs: number
|
|
60
|
+
outputTokens: number
|
|
61
|
+
sampled: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fold assistant nodes into per-turn footer metrics.
|
|
66
|
+
*
|
|
67
|
+
* TTFT is the turn's lowest-step request-dispatch-to-first-token reading, so
|
|
68
|
+
* it is only meaningful when the turn's start is inside
|
|
69
|
+
* the loaded window (the caller gates on `turnTimings`, which shares that
|
|
70
|
+
* window). Throughput divides summed output tokens by summed decode wall time,
|
|
71
|
+
* counting only steps that carry both.
|
|
72
|
+
* @param nodes - Snapshot nodes of the loaded window.
|
|
73
|
+
* @returns Turn number → available metrics; turns with none are absent.
|
|
74
|
+
*/
|
|
75
|
+
export function deriveTurnMetrics(nodes: readonly ConversationNode[]): Map<number, TurnMetrics> {
|
|
76
|
+
const folds = new Map<number, TurnFold>()
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (node.kind !== 'assistant') continue
|
|
79
|
+
const reading = assistantStepReading(node)
|
|
80
|
+
let fold = folds.get(node.turn)
|
|
81
|
+
if (fold === undefined) {
|
|
82
|
+
fold = { firstStep: node.step, firstStepTtftMs: reading.ttftMs, decodeMs: 0, outputTokens: 0, sampled: false }
|
|
83
|
+
folds.set(node.turn, fold)
|
|
84
|
+
} else if (node.step < fold.firstStep) {
|
|
85
|
+
fold.firstStep = node.step
|
|
86
|
+
fold.firstStepTtftMs = reading.ttftMs
|
|
87
|
+
}
|
|
88
|
+
if (reading.decodeMs !== null && reading.outputTokens !== null) {
|
|
89
|
+
fold.decodeMs += reading.decodeMs
|
|
90
|
+
fold.outputTokens += reading.outputTokens
|
|
91
|
+
fold.sampled = true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const metrics = new Map<number, TurnMetrics>()
|
|
95
|
+
for (const [turn, fold] of folds) {
|
|
96
|
+
const entry: TurnMetrics = {}
|
|
97
|
+
if (fold.firstStepTtftMs !== null) entry.ttftMs = fold.firstStepTtftMs
|
|
98
|
+
if (fold.sampled && fold.decodeMs > 0) entry.tokensPerSecond = fold.outputTokens / (fold.decodeMs / 1000)
|
|
99
|
+
if (entry.ttftMs !== undefined || entry.tokensPerSecond !== undefined) metrics.set(turn, entry)
|
|
100
|
+
}
|
|
101
|
+
return metrics
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use-calendar-day:抄自 DSH `packages/client/ui-conversation/src/client/chat/use-calendar-day.ts`。
|
|
3
|
+
* 组件本地日历日 tick:memoized 消息行跨午夜保持稳定 props,IconActions 时钟需要
|
|
4
|
+
* 一个本地日座,在下一个本地午夜重新触发而不触及框架 hooks。逐字保留。
|
|
5
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/use-calendar-day
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import { msUntilNextLocalMidnight, startOfLocalDay } from './message-chrome.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Local calendar-day epoch that advances at each local midnight.
|
|
12
|
+
* @returns Midnight ms for the current local day; updates after the boundary.
|
|
13
|
+
*/
|
|
14
|
+
export function useCalendarDay(): number {
|
|
15
|
+
const [day, setDay] = useState(() => startOfLocalDay(Date.now()))
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let timer: ReturnType<typeof setTimeout>
|
|
18
|
+
const arm = (): void => {
|
|
19
|
+
const now = Date.now()
|
|
20
|
+
setDay(startOfLocalDay(now))
|
|
21
|
+
timer = setTimeout(arm, msUntilNextLocalMidnight(now))
|
|
22
|
+
}
|
|
23
|
+
timer = setTimeout(arm, msUntilNextLocalMidnight(Date.now()))
|
|
24
|
+
return () => { clearTimeout(timer) }
|
|
25
|
+
}, [])
|
|
26
|
+
return day
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use-throttled-visual-update:抄自 DSH `packages/client/ui-conversation/src/client/chat/use-throttled-visual-update.ts`。
|
|
3
|
+
* 非必需视觉对齐的帧节流调度(ReasoningRow 摘要滚动用)。逐字保留。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/chat/use-throttled-visual-update
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useLayoutEffect, useRef } from 'react'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_INTERVAL_FRAMES = 3
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Return a stable scheduler that coalesces visual updates over a frame interval.
|
|
12
|
+
* @param update - DOM alignment to run after the throttle interval.
|
|
13
|
+
* @param intervalFrames - frames to wait before applying the latest alignment.
|
|
14
|
+
* @returns a stable function that schedules the latest update.
|
|
15
|
+
*/
|
|
16
|
+
export function useThrottledVisualUpdate(
|
|
17
|
+
update: () => void,
|
|
18
|
+
intervalFrames = DEFAULT_INTERVAL_FRAMES,
|
|
19
|
+
): () => void {
|
|
20
|
+
const updateRef = useRef(update)
|
|
21
|
+
updateRef.current = update
|
|
22
|
+
const pendingFrameRef = useRef<number | null>(null)
|
|
23
|
+
|
|
24
|
+
useLayoutEffect(() => () => {
|
|
25
|
+
if (pendingFrameRef.current === null) return
|
|
26
|
+
cancelAnimationFrame(pendingFrameRef.current)
|
|
27
|
+
pendingFrameRef.current = null
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
return useCallback(() => {
|
|
31
|
+
if (pendingFrameRef.current !== null) return
|
|
32
|
+
let remainingFrames = intervalFrames
|
|
33
|
+
const advance = (): void => {
|
|
34
|
+
remainingFrames -= 1
|
|
35
|
+
if (remainingFrames > 0) {
|
|
36
|
+
pendingFrameRef.current = requestAnimationFrame(advance)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
pendingFrameRef.current = null
|
|
40
|
+
updateRef.current()
|
|
41
|
+
}
|
|
42
|
+
pendingFrameRef.current = requestAnimationFrame(advance)
|
|
43
|
+
}, [intervalFrames])
|
|
44
|
+
}
|