fix(ui-conversation): share MessageIconActions to clear jscpd clone
User and assistant chrome both rendered copy/branch buttons; one shared row owns the chrome and keeps clock placement / edit as props.
This commit is contained in:
@@ -28,55 +28,17 @@
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Finalized footer: copy / branch / clock (figma 43:32997). */
|
/* Finalized footer offset (figma 43:32997); chrome lives in MessageIconActions. */
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
height: 28px;
|
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
/* Optical align with 28px icon hit targets that pad 6px past the glyph. */
|
/* Optical align with 28px icon hit targets that pad 6px past the glyph. */
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clock after the icon buttons; pl 12 separates it from branch. */
|
/* Hover-capable pointers: reveal shared actions on root hover/focus. */
|
||||||
.time {
|
|
||||||
padding-left: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 24px;
|
|
||||||
color: var(--dsw-alias-label-tertiary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover-capable pointers: hide until the root is hovered/focused. Touch /
|
|
||||||
hover:none keeps actions visible (opacity:0 still hit-tests). */
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.actions {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.root:hover .actions,
|
.root:hover .actions,
|
||||||
.root:focus-within .actions {
|
.root:focus-within .actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 6px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 28px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--dsw-alias-label-tertiary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:hover {
|
|
||||||
background: var(--dsw-alias-interactive-bg-hover);
|
|
||||||
color: var(--dsw-alias-label-secondary);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,14 +6,12 @@
|
|||||||
// the turn-level loading dots live in the chat view's tail, not here.
|
// the turn-level loading dots live in the chat view's tail, not here.
|
||||||
// Finalized nodes append IconActions (copy / branch / clock) once streaming ends.
|
// Finalized nodes append IconActions (copy / branch / clock) once streaming ends.
|
||||||
|
|
||||||
import { memo, useCallback } from 'react'
|
import { memo } from 'react'
|
||||||
import type { AssistantBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { AssistantBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import {
|
import {
|
||||||
IconBranchOutline16, IconCopyOutline16, IconThinkOutline14,
|
IconThinkOutline14, JsonBlock, MarkdownText,
|
||||||
JsonBlock, MarkdownText, Tooltip,
|
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import { formatMessageClock, writeClipboard } from './message-chrome.ts'
|
import { MessageIconActions } from './MessageIconActions.tsx'
|
||||||
import { useCalendarDay } from './use-calendar-day.ts'
|
|
||||||
import { ToolRow } from './ToolRow.tsx'
|
import { ToolRow } from './ToolRow.tsx'
|
||||||
import css from './AssistantMarkdown.module.css'
|
import css from './AssistantMarkdown.module.css'
|
||||||
|
|
||||||
@@ -55,29 +53,6 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Finalized assistant IconActions (figma 43:32997): copy live; branch stub; clock. */
|
|
||||||
function AssistantActions({ text, time }: { text: string; time: number }) {
|
|
||||||
const day = useCalendarDay()
|
|
||||||
const onCopy = useCallback(() => {
|
|
||||||
void writeClipboard(text)
|
|
||||||
}, [text])
|
|
||||||
return (
|
|
||||||
<div className={css.actions}>
|
|
||||||
<Tooltip label="复制" side="bottom">
|
|
||||||
<button type="button" className={css.action} aria-label="复制" onClick={onCopy}>
|
|
||||||
<IconCopyOutline16 />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="在新对话中分支" side="bottom">
|
|
||||||
<button type="button" className={css.action} aria-label="在新对话中分支">
|
|
||||||
<IconBranchOutline16 />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<span className={css.time}>{formatMessageClock(time, day)}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||||
blocks, streaming, interrupted, time,
|
blocks, streaming, interrupted, time,
|
||||||
}: AssistantMarkdownProps) {
|
}: AssistantMarkdownProps) {
|
||||||
@@ -105,7 +80,14 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
|||||||
})}
|
})}
|
||||||
{interrupted && <span className={css.stopped}>已停止</span>}
|
{interrupted && <span className={css.stopped}>已停止</span>}
|
||||||
</div>
|
</div>
|
||||||
{showActions && <AssistantActions text={copyText(blocks)} time={time} />}
|
{showActions && (
|
||||||
|
<MessageIconActions
|
||||||
|
text={copyText(blocks)}
|
||||||
|
time={time}
|
||||||
|
clock="end"
|
||||||
|
className={css.actions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/* Shared message IconActions row (user + assistant). Parent modules own
|
||||||
|
hover-reveal selectors and layout offsets via the composed className. */
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clock before icons (user figma 388:20051) / after (assistant 43:32997). */
|
||||||
|
.timeStart {
|
||||||
|
padding-right: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--dsw-alias-label-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeEnd {
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--dsw-alias-label-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover-capable pointers: hide until a parent hover/focus rule reveals. */
|
||||||
|
@media (hover: hover) {
|
||||||
|
.actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 28px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--dsw-alias-label-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:hover {
|
||||||
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
|
color: var(--dsw-alias-label-secondary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Shared IconActions chrome for user and assistant messages: copy / branch
|
||||||
|
// live (branch still a stub), date-aware clock, optional edit stub.
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
IconBranchOutline16, IconCopyOutline16, IconEditOutline16, Tooltip,
|
||||||
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
|
import { formatMessageClock, writeClipboard } from './message-chrome.ts'
|
||||||
|
import { useCalendarDay } from './use-calendar-day.ts'
|
||||||
|
import css from './MessageIconActions.module.css'
|
||||||
|
|
||||||
|
export interface MessageIconActionsProps {
|
||||||
|
/** Plain text the copy action writes. */
|
||||||
|
text: string
|
||||||
|
/** Unix epoch ms for the clock label. */
|
||||||
|
time: number
|
||||||
|
/** Clock before icons (user) or after (assistant). */
|
||||||
|
clock: 'start' | 'end'
|
||||||
|
/** When true, append the stub edit control (user bubble). */
|
||||||
|
edit?: boolean | undefined
|
||||||
|
/** Parent layout / hover-reveal class composed onto the actions row. */
|
||||||
|
className?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy / branch (/ clock) IconActions row shared by user and assistant chrome.
|
||||||
|
* @param props - Copy text, event time, clock side, optional edit, className.
|
||||||
|
* @returns The actions row element.
|
||||||
|
*/
|
||||||
|
export function MessageIconActions({
|
||||||
|
text, time, clock, edit, className,
|
||||||
|
}: MessageIconActionsProps) {
|
||||||
|
const day = useCalendarDay()
|
||||||
|
const onCopy = useCallback(() => {
|
||||||
|
void writeClipboard(text)
|
||||||
|
}, [text])
|
||||||
|
const clockEl = (
|
||||||
|
<span className={clock === 'start' ? css.timeStart : css.timeEnd}>
|
||||||
|
{formatMessageClock(time, day)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div className={className === undefined ? css.actions : `${css.actions} ${className}`}>
|
||||||
|
{clock === 'start' ? clockEl : null}
|
||||||
|
<Tooltip label="复制" side="bottom">
|
||||||
|
<button type="button" className={css.action} aria-label="复制" onClick={onCopy}>
|
||||||
|
<IconCopyOutline16 />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="在新对话中分支" side="bottom">
|
||||||
|
<button type="button" className={css.action} aria-label="在新对话中分支">
|
||||||
|
<IconBranchOutline16 />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{edit === true && (
|
||||||
|
<Tooltip label="编辑" side="bottom">
|
||||||
|
<button type="button" className={css.action} aria-label="编辑">
|
||||||
|
<IconEditOutline16 />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{clock === 'end' ? clockEl : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,55 +20,14 @@
|
|||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
/* Hover-capable pointers: reveal shared MessageIconActions on row hover/focus. */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Clock before the icon buttons (figma 388:20051); pr 12 separates it from copy. */
|
|
||||||
.time {
|
|
||||||
padding-right: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 24px;
|
|
||||||
color: var(--dsw-alias-label-tertiary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover-capable pointers: hide until the row is hovered/focused. Touch /
|
|
||||||
hover:none keeps actions visible (opacity:0 still hit-tests). */
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.actions {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userRow:hover .actions,
|
.userRow:hover .actions,
|
||||||
.userRow:focus-within .actions {
|
.userRow:focus-within .actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 6px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 28px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--dsw-alias-label-tertiary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:hover {
|
|
||||||
background: var(--dsw-alias-interactive-bg-hover);
|
|
||||||
color: var(--dsw-alias-label-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
// the snapshot cache; memo holds across streaming because unchanged nodes
|
// the snapshot cache; memo holds across streaming because unchanged nodes
|
||||||
// keep their references.
|
// keep their references.
|
||||||
|
|
||||||
import { memo, useCallback } from 'react'
|
import { memo } from 'react'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type {
|
import type {
|
||||||
ContextMessageNode, SteeringMessageNode, UnknownSurfaceNode, UserMessageNode,
|
ContextMessageNode, SteeringMessageNode, UnknownSurfaceNode, UserMessageNode,
|
||||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import {
|
import { JsonBlock, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
IconBranchOutline16, IconCopyOutline16, IconEditOutline16,
|
import { MessageIconActions } from './MessageIconActions.tsx'
|
||||||
JsonBlock, MessageText, Tooltip,
|
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
||||||
import { formatMessageClock, writeClipboard } from './message-chrome.ts'
|
|
||||||
import { useCalendarDay } from './use-calendar-day.ts'
|
|
||||||
import css from './MessageItem.module.css'
|
import css from './MessageItem.module.css'
|
||||||
|
|
||||||
export interface MessageItemProps {
|
export interface MessageItemProps {
|
||||||
@@ -64,34 +60,6 @@ function projectUserText(text: string): ReactNode {
|
|||||||
return <>{parts}</>
|
return <>{parts}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User-bubble IconActions (figma 388:20051): clock + copy live; branch/edit stubs. */
|
|
||||||
function UserActions({ text, time }: { text: string; time: number }) {
|
|
||||||
const day = useCalendarDay()
|
|
||||||
const onCopy = useCallback(() => {
|
|
||||||
void writeClipboard(text)
|
|
||||||
}, [text])
|
|
||||||
return (
|
|
||||||
<div className={css.actions}>
|
|
||||||
<span className={css.time}>{formatMessageClock(time, day)}</span>
|
|
||||||
<Tooltip label="复制" side="bottom">
|
|
||||||
<button type="button" className={css.action} aria-label="复制" onClick={onCopy}>
|
|
||||||
<IconCopyOutline16 />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="在新对话中分支" side="bottom">
|
|
||||||
<button type="button" className={css.action} aria-label="在新对话中分支">
|
|
||||||
<IconBranchOutline16 />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="编辑" side="bottom">
|
|
||||||
<button type="button" className={css.action} aria-label="编辑">
|
|
||||||
<IconEditOutline16 />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MessageItem = memo(function MessageItem({ node }: MessageItemProps) {
|
export const MessageItem = memo(function MessageItem({ node }: MessageItemProps) {
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case 'user': {
|
case 'user': {
|
||||||
@@ -102,7 +70,13 @@ export const MessageItem = memo(function MessageItem({ node }: MessageItemProps)
|
|||||||
{projectUserText(text)}
|
{projectUserText(text)}
|
||||||
{rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
|
{rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
|
||||||
</div>
|
</div>
|
||||||
<UserActions text={text} time={node.time} />
|
<MessageIconActions
|
||||||
|
text={text}
|
||||||
|
time={node.time}
|
||||||
|
clock="start"
|
||||||
|
edit
|
||||||
|
className={css.actions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user