Skip to content

Commit bfa4336

Browse files
authored
Merge pull request #861 from 2379278408/fix/852-copy-user-input
fix(frontend): add copy action to user messages
2 parents 5bffb7f + 3e49afa commit bfa4336

3 files changed

Lines changed: 85 additions & 40 deletions

File tree

frontend/src/components/console/task/message-userinput.tsx

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ import ReactMarkdown from "react-markdown"
22
import remarkGfm from "remark-gfm"
33
import "@/utils/plain-text-markdown.css"
44
import type { MessageType } from "./message"
5-
import { IconFile } from "@tabler/icons-react"
5+
import { IconCopy, IconFile } from "@tabler/icons-react"
66
import { isTaskImageAttachment, type TaskUserInputAttachment } from "./task-shared"
77
import React from "react"
88
import { TaskAttachmentPreviewDialog, type TaskAttachmentPreviewFile } from "./task-attachment-preview-dialog"
9+
import { Button } from "@/components/ui/button"
10+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
11+
import { toast } from "sonner"
12+
import { useTranslation } from "react-i18next"
913

1014
export const UserInputMessageItem = ({ message }: { message: MessageType }) => {
15+
const { t } = useTranslation()
1116
const [previewFile, setPreviewFile] = React.useState<TaskAttachmentPreviewFile | null>(null)
17+
const content = typeof message.data.content === "string" ? message.data.content : ""
1218
const attachments = Array.isArray(message.data.attachments)
1319
? message.data.attachments.filter((attachment): attachment is TaskUserInputAttachment => (
1420
!!attachment
@@ -19,47 +25,80 @@ export const UserInputMessageItem = ({ message }: { message: MessageType }) => {
1925
))
2026
: []
2127

28+
const handleCopy = async () => {
29+
if (!content) return
30+
31+
try {
32+
await navigator.clipboard.writeText(content)
33+
toast.success(t("taskDetail.messages.copyUserInputSuccess"))
34+
} catch (error) {
35+
toast.error(t("taskDetail.messages.copyUserInputFailed"))
36+
console.error("Copy user input failed:", error)
37+
}
38+
}
39+
2240
return (
23-
<div className="flex flex-col w-fit rounded-md bg-accent/50 px-4 py-3 max-w-[80%] break-all">
24-
{message.data.content && (
25-
<div className="user-message-markdown break-all">
26-
<ReactMarkdown
27-
remarkPlugins={[remarkGfm]}
28-
components={{
29-
p({children, ...props}) {
30-
if (typeof children === 'string') {
31-
return (children as string).split('\n').map((line: string, index: number) => (
32-
<p key={index} {...props}>{line}</p>
33-
))
34-
} else {
35-
return <p {...props}>{children}</p>
41+
<div className="group/user-input relative inline-flex w-fit max-w-[80%] flex-col pb-7">
42+
<div className="flex flex-col rounded-md bg-accent/50 px-4 py-3 break-all">
43+
{content && (
44+
<div className="user-message-markdown break-all">
45+
<ReactMarkdown
46+
remarkPlugins={[remarkGfm]}
47+
components={{
48+
p({children, ...props}) {
49+
if (typeof children === 'string') {
50+
return (children as string).split('\n').map((line: string, index: number) => (
51+
<p key={index} {...props}>{line}</p>
52+
))
53+
} else {
54+
return <p {...props}>{children}</p>
55+
}
3656
}
37-
}
38-
}}>
39-
{message.data.content}
40-
</ReactMarkdown>
41-
</div>
42-
)}
43-
{attachments.length > 0 && (
44-
<div className="mt-2 flex flex-col gap-1.5">
45-
{attachments.map((attachment, index) => (
46-
<button
47-
type="button"
48-
key={`${attachment.url}-${index}`}
49-
className="group/attachment flex max-w-full cursor-pointer items-center gap-2 rounded-md border bg-background/70 px-2 py-1.5 text-left text-xs text-foreground hover:bg-background"
50-
onClick={() => setPreviewFile({
51-
name: attachment.filename,
52-
accessUrl: attachment.url,
53-
})}
54-
>
55-
{isTaskImageAttachment(attachment.filename) ? (
56-
<img src={attachment.url} alt={attachment.filename} className="size-4 shrink-0 rounded object-cover" />
57-
) : (
58-
<IconFile className="size-4 shrink-0 text-muted-foreground" />
59-
)}
60-
<span className="min-w-0 flex-1 truncate group-hover/attachment:text-primary">{attachment.filename}</span>
61-
</button>
62-
))}
57+
}}>
58+
{content}
59+
</ReactMarkdown>
60+
</div>
61+
)}
62+
{attachments.length > 0 && (
63+
<div className="mt-2 flex flex-col gap-1.5">
64+
{attachments.map((attachment, index) => (
65+
<button
66+
type="button"
67+
key={`${attachment.url}-${index}`}
68+
className="group/attachment flex max-w-full cursor-pointer items-center gap-2 rounded-md border bg-background/70 px-2 py-1.5 text-left text-xs text-foreground hover:bg-background"
69+
onClick={() => setPreviewFile({
70+
name: attachment.filename,
71+
accessUrl: attachment.url,
72+
})}
73+
>
74+
{isTaskImageAttachment(attachment.filename) ? (
75+
<img src={attachment.url} alt={attachment.filename} className="size-4 shrink-0 rounded object-cover" />
76+
) : (
77+
<IconFile className="size-4 shrink-0 text-muted-foreground" />
78+
)}
79+
<span className="min-w-0 flex-1 truncate group-hover/attachment:text-primary">{attachment.filename}</span>
80+
</button>
81+
))}
82+
</div>
83+
)}
84+
</div>
85+
{content && (
86+
<div className="absolute bottom-0 right-0 flex justify-end opacity-0 transition-opacity group-hover/user-input:opacity-100 group-focus-within/user-input:opacity-100">
87+
<Tooltip>
88+
<TooltipTrigger asChild>
89+
<Button
90+
type="button"
91+
variant="ghost"
92+
size="icon-sm"
93+
className="size-6 text-muted-foreground hover:text-foreground"
94+
aria-label={t("taskDetail.messages.copyUserInput")}
95+
onClick={handleCopy}
96+
>
97+
<IconCopy className="size-3.5" />
98+
</Button>
99+
</TooltipTrigger>
100+
<TooltipContent>{t("taskDetail.messages.copyUserInput")}</TooltipContent>
101+
</Tooltip>
63102
</div>
64103
)}
65104
<TaskAttachmentPreviewDialog

frontend/src/i18n/resources/cn.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4073,6 +4073,9 @@ const cn = {
40734073
loadingHistory: "正在加载",
40744074
loadMore: "加载更多",
40754075
loadHistory: "加载历史消息",
4076+
copyUserInput: "复制用户输入",
4077+
copyUserInputSuccess: "用户输入已复制到剪贴板",
4078+
copyUserInputFailed: "复制用户输入失败",
40764079
},
40774080
plan: {
40784081
title: "执行步骤 ({{completed}}/{{total}})",

frontend/src/i18n/resources/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4073,6 +4073,9 @@ const en = {
40734073
loadingHistory: "Loading",
40744074
loadMore: "Load more",
40754075
loadHistory: "Load history",
4076+
copyUserInput: "Copy user input",
4077+
copyUserInputSuccess: "User input copied to clipboard",
4078+
copyUserInputFailed: "Failed to copy user input",
40764079
},
40774080
plan: {
40784081
title: "Steps ({{completed}}/{{total}})",

0 commit comments

Comments
 (0)