Browse Source

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.
07akioni 1 month ago
parent
commit
c8ea9a5204

+ 2 - 40
packages/client/ui-conversation/src/client/chat/AssistantMarkdown.module.css

@@ -28,55 +28,17 @@
   line-height: 18px;
 }
 
-/* Finalized footer: copy / branch / clock (figma 43:32997). */
+/* Finalized footer offset (figma 43:32997); chrome lives in MessageIconActions. */
 .actions {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  height: 28px;
   margin-top: 16px;
   /* Optical align with 28px icon hit targets that pad 6px past the glyph. */
   margin-left: -6px;
 }
 
-/* Clock after the icon buttons; pl 12 separates it from branch. */
-.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). */
+/* Hover-capable pointers: reveal shared actions on root hover/focus. */
 @media (hover: hover) {
-  .actions {
-    opacity: 0;
-    transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
-  }
-
   .root:hover .actions,
   .root:focus-within .actions {
     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);
-}

+ 11 - 29
packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx

@@ -6,14 +6,12 @@
 // the turn-level loading dots live in the chat view's tail, not here.
 // 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 {
-  IconBranchOutline16, IconCopyOutline16, IconThinkOutline14,
-  JsonBlock, MarkdownText, Tooltip,
+  IconThinkOutline14, JsonBlock, MarkdownText,
 } from '@deepseek-ai/dsh-client-ui-primitives'
-import { formatMessageClock, writeClipboard } from './message-chrome.ts'
-import { useCalendarDay } from './use-calendar-day.ts'
+import { MessageIconActions } from './MessageIconActions.tsx'
 import { ToolRow } from './ToolRow.tsx'
 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({
   blocks, streaming, interrupted, time,
 }: AssistantMarkdownProps) {
@@ -105,7 +80,14 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
         })}
         {interrupted && <span className={css.stopped}>已停止</span>}
       </div>
-      {showActions && <AssistantActions text={copyText(blocks)} time={time} />}
+      {showActions && (
+        <MessageIconActions
+          text={copyText(blocks)}
+          time={time}
+          clock="end"
+          className={css.actions}
+        />
+      )}
     </div>
   )
 })

+ 53 - 0
packages/client/ui-conversation/src/client/chat/MessageIconActions.module.css

@@ -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);
+}

+ 65 - 0
packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx

@@ -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>
+  )
+}

+ 1 - 42
packages/client/ui-conversation/src/client/chat/MessageItem.module.css

@@ -20,55 +20,14 @@
   color: var(--dsw-alias-label-primary);
 }
 
-.actions {
-  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). */
+/* Hover-capable pointers: reveal shared MessageIconActions on row hover/focus. */
 @media (hover: hover) {
-  .actions {
-    opacity: 0;
-    transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out);
-  }
-
   .userRow:hover .actions,
   .userRow:focus-within .actions {
     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 {
   display: inline-block;
   margin-bottom: 4px;

+ 10 - 36
packages/client/ui-conversation/src/client/chat/MessageItem.tsx

@@ -4,17 +4,13 @@
 // the snapshot cache; memo holds across streaming because unchanged nodes
 // keep their references.
 
-import { memo, useCallback } from 'react'
+import { memo } from 'react'
 import type { ReactNode } from 'react'
 import type {
   ContextMessageNode, SteeringMessageNode, UnknownSurfaceNode, UserMessageNode,
 } from '@deepseek-ai/dsh-client-runtime/client'
-import {
-  IconBranchOutline16, IconCopyOutline16, IconEditOutline16,
-  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 { JsonBlock, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
+import { MessageIconActions } from './MessageIconActions.tsx'
 import css from './MessageItem.module.css'
 
 export interface MessageItemProps {
@@ -64,34 +60,6 @@ function projectUserText(text: string): ReactNode {
   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) {
   switch (node.kind) {
     case 'user': {
@@ -102,7 +70,13 @@ export const MessageItem = memo(function MessageItem({ node }: MessageItemProps)
             {projectUserText(text)}
             {rest.map((block, i) => <JsonBlock key={i} label="附加内容块" payload={block} />)}
           </div>
-          <UserActions text={text} time={node.time} />
+          <MessageIconActions
+            text={text}
+            time={node.time}
+            clock="start"
+            edit
+            className={css.actions}
+          />
         </div>
       )
     }