v0.1.4-p4
parent
3ca7eff38b
commit
08b35d632b
|
|
@ -216,11 +216,10 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px dashed color-mix(in oklab, #6f94ff 46%, var(--line) 54%);
|
border: 1px dashed color-mix(in oklab, #aeb7c4 52%, var(--line) 48%);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background:
|
background: color-mix(in oklab, #eef1f4 82%, var(--panel) 18%);
|
||||||
linear-gradient(180deg, color-mix(in oklab, var(--panel) 68%, #dfe9ff 32%), color-mix(in oklab, var(--panel) 82%, #d5e3ff 18%));
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-staged-submission-item {
|
.ops-staged-submission-item {
|
||||||
|
|
@ -229,9 +228,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid color-mix(in oklab, #7aa2ff 18%, var(--line) 82%);
|
border: 1px solid color-mix(in oklab, #bcc5cf 26%, var(--line) 74%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: color-mix(in oklab, var(--panel) 84%, white 16%);
|
background: color-mix(in oklab, #f5f7f9 88%, var(--panel) 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-staged-submission-index {
|
.ops-staged-submission-index {
|
||||||
|
|
@ -242,9 +241,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--brand);
|
color: color-mix(in oklab, var(--text) 72%, var(--muted) 28%);
|
||||||
background: color-mix(in oklab, var(--panel) 64%, white 36%);
|
background: color-mix(in oklab, #ffffff 72%, #e5e9ee 28%);
|
||||||
border: 1px solid color-mix(in oklab, var(--line) 70%, transparent);
|
border: 1px solid color-mix(in oklab, #c7ced6 46%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-staged-submission-body {
|
.ops-staged-submission-body {
|
||||||
|
|
@ -272,12 +271,12 @@
|
||||||
.ops-staged-submission-pill {
|
.ops-staged-submission-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid color-mix(in oklab, var(--line) 74%, transparent);
|
border: 1px solid color-mix(in oklab, #c3cad2 44%, transparent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--subtitle);
|
color: var(--subtitle);
|
||||||
background: color-mix(in oklab, var(--panel) 82%, transparent);
|
background: color-mix(in oklab, #ffffff 74%, #e8edf2 26%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-staged-submission-actions {
|
.ops-staged-submission-actions {
|
||||||
|
|
@ -290,9 +289,9 @@
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid color-mix(in oklab, var(--line) 76%, transparent);
|
border: 1px solid color-mix(in oklab, #c2c9d2 42%, transparent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: color-mix(in oklab, var(--panel) 88%, white 12%);
|
background: color-mix(in oklab, #fbfcfd 82%, #e6ebf0 18%);
|
||||||
color: var(--icon-muted);
|
color: var(--icon-muted);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -303,8 +302,8 @@
|
||||||
|
|
||||||
.ops-staged-submission-icon-btn:hover:not(:disabled) {
|
.ops-staged-submission-icon-btn:hover:not(:disabled) {
|
||||||
color: var(--icon);
|
color: var(--icon);
|
||||||
background: color-mix(in oklab, var(--panel) 70%, var(--brand-soft) 30%);
|
background: color-mix(in oklab, #edf1f5 78%, #d8dde4 22%);
|
||||||
border-color: color-mix(in oklab, var(--brand) 38%, var(--line) 62%);
|
border-color: color-mix(in oklab, #97a3af 34%, var(--line) 66%);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Pencil, Plus, RefreshCw, RotateCcw, Square, Trash2, X } from 'lucide-react';
|
import { ArrowUp, ChevronLeft, Clock3, Command, Download, Eye, FileText, Mic, Paperclip, Pencil, Plus, RefreshCw, RotateCcw, Square, Trash2, X } from 'lucide-react';
|
||||||
import type { Components } from 'react-markdown';
|
import type { Components } from 'react-markdown';
|
||||||
import type { ChangeEventHandler, KeyboardEventHandler, RefObject } from 'react';
|
import { memo, type ChangeEventHandler, type KeyboardEventHandler, type RefObject } from 'react';
|
||||||
|
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
||||||
|
|
@ -121,6 +121,145 @@ interface DashboardChatPanelProps {
|
||||||
onSubmitAction: () => Promise<void> | void;
|
onSubmitAction: () => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DashboardChatTranscriptProps {
|
||||||
|
conversation: ChatMessage[];
|
||||||
|
isZh: boolean;
|
||||||
|
labels: DashboardChatPanelLabels;
|
||||||
|
chatScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
|
onChatScroll: () => void;
|
||||||
|
expandedProgressByKey: Record<string, boolean>;
|
||||||
|
expandedUserByKey: Record<string, boolean>;
|
||||||
|
deletingMessageIdMap: Record<number, boolean>;
|
||||||
|
feedbackSavingByMessageId: Record<number, boolean>;
|
||||||
|
markdownComponents: Components;
|
||||||
|
workspaceDownloadExtensionSet: ReadonlySet<string>;
|
||||||
|
onToggleProgressExpand: (key: string) => void;
|
||||||
|
onToggleUserExpand: (key: string) => void;
|
||||||
|
onEditUserPrompt: (text: string) => void;
|
||||||
|
onCopyUserPrompt: (text: string) => Promise<void> | void;
|
||||||
|
onDeleteConversationMessage: (message: ChatMessage) => Promise<void> | void;
|
||||||
|
onOpenWorkspacePath: (path: string) => Promise<void> | void;
|
||||||
|
onSubmitAssistantFeedback: (message: ChatMessage, direction: 'up' | 'down') => Promise<void> | void;
|
||||||
|
onQuoteAssistantReply: (message: ChatMessage) => void;
|
||||||
|
onCopyAssistantReply: (text: string) => Promise<void> | void;
|
||||||
|
isThinking: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
||||||
|
conversation,
|
||||||
|
isZh,
|
||||||
|
labels,
|
||||||
|
chatScrollRef,
|
||||||
|
onChatScroll,
|
||||||
|
expandedProgressByKey,
|
||||||
|
expandedUserByKey,
|
||||||
|
deletingMessageIdMap,
|
||||||
|
feedbackSavingByMessageId,
|
||||||
|
markdownComponents,
|
||||||
|
workspaceDownloadExtensionSet,
|
||||||
|
onToggleProgressExpand,
|
||||||
|
onToggleUserExpand,
|
||||||
|
onEditUserPrompt,
|
||||||
|
onCopyUserPrompt,
|
||||||
|
onDeleteConversationMessage,
|
||||||
|
onOpenWorkspacePath,
|
||||||
|
onSubmitAssistantFeedback,
|
||||||
|
onQuoteAssistantReply,
|
||||||
|
onCopyAssistantReply,
|
||||||
|
isThinking,
|
||||||
|
}: DashboardChatTranscriptProps) {
|
||||||
|
return (
|
||||||
|
<div className="ops-chat-scroll" ref={chatScrollRef} onScroll={onChatScroll}>
|
||||||
|
{conversation.length === 0 ? (
|
||||||
|
<div className="ops-chat-empty">
|
||||||
|
{labels.noConversation}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DashboardConversationMessages
|
||||||
|
conversation={conversation}
|
||||||
|
isZh={isZh}
|
||||||
|
labels={{
|
||||||
|
badReply: labels.badReply,
|
||||||
|
copyPrompt: labels.copyPrompt,
|
||||||
|
copyReply: labels.copyReply,
|
||||||
|
deleteMessage: labels.deleteMessage,
|
||||||
|
download: labels.download,
|
||||||
|
editPrompt: labels.editPrompt,
|
||||||
|
fileNotPreviewable: labels.fileNotPreviewable,
|
||||||
|
goodReply: labels.goodReply,
|
||||||
|
previewTitle: labels.previewTitle,
|
||||||
|
quoteReply: labels.quoteReply,
|
||||||
|
quotedReplyLabel: labels.quotedReplyLabel,
|
||||||
|
user: labels.user,
|
||||||
|
you: labels.you,
|
||||||
|
}}
|
||||||
|
expandedProgressByKey={expandedProgressByKey}
|
||||||
|
expandedUserByKey={expandedUserByKey}
|
||||||
|
deletingMessageIdMap={deletingMessageIdMap}
|
||||||
|
feedbackSavingByMessageId={feedbackSavingByMessageId}
|
||||||
|
markdownComponents={markdownComponents}
|
||||||
|
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
||||||
|
onToggleProgressExpand={onToggleProgressExpand}
|
||||||
|
onToggleUserExpand={onToggleUserExpand}
|
||||||
|
onEditUserPrompt={onEditUserPrompt}
|
||||||
|
onCopyUserPrompt={onCopyUserPrompt}
|
||||||
|
onDeleteConversationMessage={onDeleteConversationMessage}
|
||||||
|
onOpenWorkspacePath={onOpenWorkspacePath}
|
||||||
|
onSubmitAssistantFeedback={onSubmitAssistantFeedback}
|
||||||
|
onQuoteAssistantReply={onQuoteAssistantReply}
|
||||||
|
onCopyAssistantReply={onCopyAssistantReply}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isThinking ? (
|
||||||
|
<div className="ops-chat-row is-assistant">
|
||||||
|
<div className="ops-chat-item is-assistant">
|
||||||
|
<div className="ops-avatar bot" title="Nanobot">
|
||||||
|
<img src={nanobotLogo} alt="Nanobot" />
|
||||||
|
</div>
|
||||||
|
<div className="ops-thinking-bubble">
|
||||||
|
<div className="ops-thinking-cloud">
|
||||||
|
<span className="dot" />
|
||||||
|
<span className="dot" />
|
||||||
|
<span className="dot" />
|
||||||
|
</div>
|
||||||
|
<div className="ops-thinking-text">{labels.thinking}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, (prev, next) => (
|
||||||
|
prev.conversation === next.conversation
|
||||||
|
&& prev.isZh === next.isZh
|
||||||
|
&& prev.isThinking === next.isThinking
|
||||||
|
&& prev.chatScrollRef === next.chatScrollRef
|
||||||
|
&& prev.expandedProgressByKey === next.expandedProgressByKey
|
||||||
|
&& prev.expandedUserByKey === next.expandedUserByKey
|
||||||
|
&& prev.deletingMessageIdMap === next.deletingMessageIdMap
|
||||||
|
&& prev.feedbackSavingByMessageId === next.feedbackSavingByMessageId
|
||||||
|
&& prev.markdownComponents === next.markdownComponents
|
||||||
|
&& prev.workspaceDownloadExtensionSet === next.workspaceDownloadExtensionSet
|
||||||
|
&& prev.labels.badReply === next.labels.badReply
|
||||||
|
&& prev.labels.copyPrompt === next.labels.copyPrompt
|
||||||
|
&& prev.labels.copyReply === next.labels.copyReply
|
||||||
|
&& prev.labels.deleteMessage === next.labels.deleteMessage
|
||||||
|
&& prev.labels.download === next.labels.download
|
||||||
|
&& prev.labels.editPrompt === next.labels.editPrompt
|
||||||
|
&& prev.labels.fileNotPreviewable === next.labels.fileNotPreviewable
|
||||||
|
&& prev.labels.goodReply === next.labels.goodReply
|
||||||
|
&& prev.labels.noConversation === next.labels.noConversation
|
||||||
|
&& prev.labels.previewTitle === next.labels.previewTitle
|
||||||
|
&& prev.labels.quoteReply === next.labels.quoteReply
|
||||||
|
&& prev.labels.quotedReplyLabel === next.labels.quotedReplyLabel
|
||||||
|
&& prev.labels.thinking === next.labels.thinking
|
||||||
|
&& prev.labels.user === next.labels.user
|
||||||
|
&& prev.labels.you === next.labels.you
|
||||||
|
));
|
||||||
|
|
||||||
export function DashboardChatPanel({
|
export function DashboardChatPanel({
|
||||||
conversation,
|
conversation,
|
||||||
isZh,
|
isZh,
|
||||||
|
|
@ -195,68 +334,29 @@ export function DashboardChatPanel({
|
||||||
const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply);
|
const hasComposerDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply);
|
||||||
return (
|
return (
|
||||||
<div className={`ops-chat-frame ${isChatEnabled ? '' : 'is-disabled'}`}>
|
<div className={`ops-chat-frame ${isChatEnabled ? '' : 'is-disabled'}`}>
|
||||||
<div className="ops-chat-scroll" ref={chatScrollRef} onScroll={onChatScroll}>
|
<MemoizedChatTranscript
|
||||||
{conversation.length === 0 ? (
|
conversation={conversation}
|
||||||
<div className="ops-chat-empty">
|
isZh={isZh}
|
||||||
{labels.noConversation}
|
labels={labels}
|
||||||
</div>
|
chatScrollRef={chatScrollRef}
|
||||||
) : (
|
onChatScroll={onChatScroll}
|
||||||
<DashboardConversationMessages
|
expandedProgressByKey={expandedProgressByKey}
|
||||||
conversation={conversation}
|
expandedUserByKey={expandedUserByKey}
|
||||||
isZh={isZh}
|
deletingMessageIdMap={deletingMessageIdMap}
|
||||||
labels={{
|
feedbackSavingByMessageId={feedbackSavingByMessageId}
|
||||||
badReply: labels.badReply,
|
markdownComponents={markdownComponents}
|
||||||
copyPrompt: labels.copyPrompt,
|
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
||||||
copyReply: labels.copyReply,
|
onToggleProgressExpand={onToggleProgressExpand}
|
||||||
deleteMessage: labels.deleteMessage,
|
onToggleUserExpand={onToggleUserExpand}
|
||||||
download: labels.download,
|
onEditUserPrompt={onEditUserPrompt}
|
||||||
editPrompt: labels.editPrompt,
|
onCopyUserPrompt={onCopyUserPrompt}
|
||||||
fileNotPreviewable: labels.fileNotPreviewable,
|
onDeleteConversationMessage={onDeleteConversationMessage}
|
||||||
goodReply: labels.goodReply,
|
onOpenWorkspacePath={onOpenWorkspacePath}
|
||||||
previewTitle: labels.previewTitle,
|
onSubmitAssistantFeedback={onSubmitAssistantFeedback}
|
||||||
quoteReply: labels.quoteReply,
|
onQuoteAssistantReply={onQuoteAssistantReply}
|
||||||
quotedReplyLabel: labels.quotedReplyLabel,
|
onCopyAssistantReply={onCopyAssistantReply}
|
||||||
user: labels.user,
|
isThinking={isThinking}
|
||||||
you: labels.you,
|
/>
|
||||||
}}
|
|
||||||
expandedProgressByKey={expandedProgressByKey}
|
|
||||||
expandedUserByKey={expandedUserByKey}
|
|
||||||
deletingMessageIdMap={deletingMessageIdMap}
|
|
||||||
feedbackSavingByMessageId={feedbackSavingByMessageId}
|
|
||||||
markdownComponents={markdownComponents}
|
|
||||||
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
|
||||||
onToggleProgressExpand={onToggleProgressExpand}
|
|
||||||
onToggleUserExpand={onToggleUserExpand}
|
|
||||||
onEditUserPrompt={onEditUserPrompt}
|
|
||||||
onCopyUserPrompt={onCopyUserPrompt}
|
|
||||||
onDeleteConversationMessage={onDeleteConversationMessage}
|
|
||||||
onOpenWorkspacePath={onOpenWorkspacePath}
|
|
||||||
onSubmitAssistantFeedback={onSubmitAssistantFeedback}
|
|
||||||
onQuoteAssistantReply={onQuoteAssistantReply}
|
|
||||||
onCopyAssistantReply={onCopyAssistantReply}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isThinking ? (
|
|
||||||
<div className="ops-chat-row is-assistant">
|
|
||||||
<div className="ops-chat-item is-assistant">
|
|
||||||
<div className="ops-avatar bot" title="Nanobot">
|
|
||||||
<img src={nanobotLogo} alt="Nanobot" />
|
|
||||||
</div>
|
|
||||||
<div className="ops-thinking-bubble">
|
|
||||||
<div className="ops-thinking-cloud">
|
|
||||||
<span className="dot" />
|
|
||||||
<span className="dot" />
|
|
||||||
<span className="dot" />
|
|
||||||
</div>
|
|
||||||
<div className="ops-thinking-text">{labels.thinking}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ops-chat-dock">
|
<div className="ops-chat-dock">
|
||||||
{stagedSubmissions.length > 0 ? (
|
{stagedSubmissions.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ChevronDown, ChevronUp, Copy, Download, Eye, FileText, Pencil, Reply, ThumbsDown, ThumbsUp, Trash2, UserRound } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Copy, Download, Eye, FileText, Pencil, Reply, ThumbsDown, ThumbsUp, Trash2, UserRound } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeSanitize from 'rehype-sanitize';
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
|
@ -50,6 +51,29 @@ interface DashboardConversationMessagesProps {
|
||||||
onCopyAssistantReply: (text: string) => Promise<void> | void;
|
onCopyAssistantReply: (text: string) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DashboardConversationMessageRowProps {
|
||||||
|
item: ChatMessage;
|
||||||
|
itemKey: string;
|
||||||
|
isZh: boolean;
|
||||||
|
labels: DashboardConversationLabels;
|
||||||
|
showDateDivider: boolean;
|
||||||
|
expandedProgress: boolean;
|
||||||
|
expandedUser: boolean;
|
||||||
|
isDeleting: boolean;
|
||||||
|
isFeedbackSaving: boolean;
|
||||||
|
markdownComponents: Components;
|
||||||
|
workspaceDownloadExtensionSet: ReadonlySet<string>;
|
||||||
|
onToggleProgressExpand: (key: string) => void;
|
||||||
|
onToggleUserExpand: (key: string) => void;
|
||||||
|
onEditUserPrompt: (text: string) => void;
|
||||||
|
onCopyUserPrompt: (text: string) => Promise<void> | void;
|
||||||
|
onDeleteConversationMessage: (message: ChatMessage) => Promise<void> | void;
|
||||||
|
onOpenWorkspacePath: (path: string) => Promise<void> | void;
|
||||||
|
onSubmitAssistantFeedback: (message: ChatMessage, direction: 'up' | 'down') => Promise<void> | void;
|
||||||
|
onQuoteAssistantReply: (message: ChatMessage) => void;
|
||||||
|
onCopyAssistantReply: (text: string) => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldCollapseProgress(text: string) {
|
function shouldCollapseProgress(text: string) {
|
||||||
const normalized = String(text || '').trim();
|
const normalized = String(text || '').trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|
@ -65,6 +89,261 @@ function getConversationItemKey(item: ChatMessage, idx: number) {
|
||||||
return `temp:${item.role}:${item.kind || 'final'}:${item.ts}:${idx}`;
|
return `temp:${item.role}:${item.kind || 'final'}:${item.ts}:${idx}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameAttachments(left?: string[], right?: string[]) {
|
||||||
|
const aa = left || [];
|
||||||
|
const bb = right || [];
|
||||||
|
if (aa.length !== bb.length) return false;
|
||||||
|
return aa.every((value, index) => value === bb[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardConversationMessageRow = memo(function DashboardConversationMessageRow({
|
||||||
|
item,
|
||||||
|
itemKey,
|
||||||
|
isZh,
|
||||||
|
labels,
|
||||||
|
showDateDivider,
|
||||||
|
expandedProgress,
|
||||||
|
expandedUser,
|
||||||
|
isDeleting,
|
||||||
|
isFeedbackSaving,
|
||||||
|
markdownComponents,
|
||||||
|
workspaceDownloadExtensionSet,
|
||||||
|
onToggleProgressExpand,
|
||||||
|
onToggleUserExpand,
|
||||||
|
onEditUserPrompt,
|
||||||
|
onCopyUserPrompt,
|
||||||
|
onDeleteConversationMessage,
|
||||||
|
onOpenWorkspacePath,
|
||||||
|
onSubmitAssistantFeedback,
|
||||||
|
onQuoteAssistantReply,
|
||||||
|
onCopyAssistantReply,
|
||||||
|
}: DashboardConversationMessageRowProps) {
|
||||||
|
const isProgressBubble = item.role !== 'user' && (item.kind || 'final') === 'progress';
|
||||||
|
const isUserBubble = item.role === 'user';
|
||||||
|
const fullText = String(item.text || '');
|
||||||
|
const summaryText = isProgressBubble ? summarizeProgressText(fullText, isZh) : fullText;
|
||||||
|
const hasSummary = isProgressBubble && summaryText.trim().length > 0 && summaryText.trim() !== fullText.trim();
|
||||||
|
const progressCollapsible = isProgressBubble && (hasSummary || shouldCollapseProgress(fullText));
|
||||||
|
const normalizedUserText = isUserBubble ? normalizeUserMessageText(fullText) : '';
|
||||||
|
const userLineCount = isUserBubble ? normalizedUserText.split('\n').length : 0;
|
||||||
|
const userCollapsible = isUserBubble && userLineCount > 5;
|
||||||
|
const collapsible = isProgressBubble ? progressCollapsible : userCollapsible;
|
||||||
|
const expanded = isProgressBubble ? expandedProgress : expandedUser;
|
||||||
|
const displayText = isProgressBubble && !expanded ? summaryText : fullText;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={itemKey}
|
||||||
|
data-chat-message-id={item.id ? String(item.id) : undefined}
|
||||||
|
>
|
||||||
|
{showDateDivider ? (
|
||||||
|
<div className="ops-chat-date-divider" aria-label={formatConversationDate(item.ts, isZh)}>
|
||||||
|
<span>{formatConversationDate(item.ts, isZh)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className={`ops-chat-row ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
||||||
|
<div className={`ops-chat-item ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
||||||
|
{item.role !== 'user' && (
|
||||||
|
<div className="ops-avatar bot" title="Nanobot">
|
||||||
|
<img src={nanobotLogo} alt="Nanobot" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.role === 'user' ? (
|
||||||
|
<div className="ops-chat-hover-actions ops-chat-hover-actions-user">
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => onEditUserPrompt(item.text)}
|
||||||
|
tooltip={labels.editPrompt}
|
||||||
|
aria-label={labels.editPrompt}
|
||||||
|
>
|
||||||
|
<Pencil size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => void onCopyUserPrompt(item.text)}
|
||||||
|
tooltip={labels.copyPrompt}
|
||||||
|
aria-label={labels.copyPrompt}
|
||||||
|
>
|
||||||
|
<Copy size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => void onDeleteConversationMessage(item)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
tooltip={labels.deleteMessage}
|
||||||
|
aria-label={labels.deleteMessage}
|
||||||
|
>
|
||||||
|
<Trash2 size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={`ops-chat-bubble ${item.role === 'user' ? 'user' : 'assistant'} ${(item.kind || 'final') === 'progress' ? 'progress' : ''}`}>
|
||||||
|
<div className="ops-chat-meta">
|
||||||
|
<span>{item.role === 'user' ? labels.you : 'Nanobot'}</span>
|
||||||
|
<div className="ops-chat-meta-right">
|
||||||
|
<span className="mono">{formatClock(item.ts)}</span>
|
||||||
|
{collapsible ? (
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-expand-icon-btn"
|
||||||
|
onClick={() => {
|
||||||
|
if (isProgressBubble) {
|
||||||
|
onToggleProgressExpand(itemKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onToggleUserExpand(itemKey);
|
||||||
|
}}
|
||||||
|
tooltip={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
||||||
|
aria-label={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
|
</LucentIconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`ops-chat-text ${collapsible && !expanded ? (isUserBubble ? 'is-collapsed-user' : 'is-collapsed') : ''}`}>
|
||||||
|
{item.text ? (
|
||||||
|
item.role === 'user' ? (
|
||||||
|
<>
|
||||||
|
{item.quoted_reply ? (
|
||||||
|
<div className="ops-user-quoted-reply">
|
||||||
|
<div className="ops-user-quoted-label">{labels.quotedReplyLabel}</div>
|
||||||
|
<div className="ops-user-quoted-text">{normalizeAssistantMessageText(item.quoted_reply)}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="whitespace-pre-wrap">{normalizeUserMessageText(displayText)}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
||||||
|
components={markdownComponents}
|
||||||
|
>
|
||||||
|
{decorateWorkspacePathsForMarkdown(displayText)}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{(item.attachments || []).length > 0 ? (
|
||||||
|
<div className="ops-chat-attachments">
|
||||||
|
{(item.attachments || []).map((rawPath) => {
|
||||||
|
const filePath = normalizeDashboardAttachmentPath(rawPath);
|
||||||
|
const fileAction = workspaceFileAction(filePath, workspaceDownloadExtensionSet);
|
||||||
|
const filename = filePath.split('/').pop() || filePath;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={`${item.ts}-${filePath}`}
|
||||||
|
className="ops-attach-link mono"
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void onOpenWorkspacePath(filePath);
|
||||||
|
}}
|
||||||
|
title={fileAction === 'download' ? labels.download : fileAction === 'preview' ? labels.previewTitle : labels.fileNotPreviewable}
|
||||||
|
>
|
||||||
|
{fileAction === 'download' ? (
|
||||||
|
<Download size={12} className="ops-attach-link-icon" />
|
||||||
|
) : fileAction === 'preview' ? (
|
||||||
|
<Eye size={12} className="ops-attach-link-icon" />
|
||||||
|
) : (
|
||||||
|
<FileText size={12} className="ops-attach-link-icon" />
|
||||||
|
)}
|
||||||
|
<span className="ops-attach-link-name">{filename}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{item.role === 'assistant' && !isProgressBubble ? (
|
||||||
|
<div className="ops-chat-reply-actions">
|
||||||
|
<LucentIconButton
|
||||||
|
className={`ops-chat-inline-action ${item.feedback === 'up' ? 'active-up' : ''}`}
|
||||||
|
onClick={() => void onSubmitAssistantFeedback(item, 'up')}
|
||||||
|
disabled={isFeedbackSaving}
|
||||||
|
tooltip={labels.goodReply}
|
||||||
|
aria-label={labels.goodReply}
|
||||||
|
>
|
||||||
|
<ThumbsUp size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className={`ops-chat-inline-action ${item.feedback === 'down' ? 'active-down' : ''}`}
|
||||||
|
onClick={() => void onSubmitAssistantFeedback(item, 'down')}
|
||||||
|
disabled={isFeedbackSaving}
|
||||||
|
tooltip={labels.badReply}
|
||||||
|
aria-label={labels.badReply}
|
||||||
|
>
|
||||||
|
<ThumbsDown size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => onQuoteAssistantReply(item)}
|
||||||
|
tooltip={labels.quoteReply}
|
||||||
|
aria-label={labels.quoteReply}
|
||||||
|
>
|
||||||
|
<Reply size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => void onCopyAssistantReply(item.text)}
|
||||||
|
tooltip={labels.copyReply}
|
||||||
|
aria-label={labels.copyReply}
|
||||||
|
>
|
||||||
|
<Copy size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="ops-chat-inline-action"
|
||||||
|
onClick={() => void onDeleteConversationMessage(item)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
tooltip={labels.deleteMessage}
|
||||||
|
aria-label={labels.deleteMessage}
|
||||||
|
>
|
||||||
|
<Trash2 size={13} />
|
||||||
|
</LucentIconButton>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.role === 'user' && (
|
||||||
|
<div className="ops-avatar user" title={labels.user}>
|
||||||
|
<UserRound size={18} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, (prev, next) => (
|
||||||
|
prev.itemKey === next.itemKey
|
||||||
|
&& prev.isZh === next.isZh
|
||||||
|
&& prev.showDateDivider === next.showDateDivider
|
||||||
|
&& prev.expandedProgress === next.expandedProgress
|
||||||
|
&& prev.expandedUser === next.expandedUser
|
||||||
|
&& prev.isDeleting === next.isDeleting
|
||||||
|
&& prev.isFeedbackSaving === next.isFeedbackSaving
|
||||||
|
&& prev.markdownComponents === next.markdownComponents
|
||||||
|
&& prev.workspaceDownloadExtensionSet === next.workspaceDownloadExtensionSet
|
||||||
|
&& prev.labels.badReply === next.labels.badReply
|
||||||
|
&& prev.labels.copyPrompt === next.labels.copyPrompt
|
||||||
|
&& prev.labels.copyReply === next.labels.copyReply
|
||||||
|
&& prev.labels.deleteMessage === next.labels.deleteMessage
|
||||||
|
&& prev.labels.download === next.labels.download
|
||||||
|
&& prev.labels.editPrompt === next.labels.editPrompt
|
||||||
|
&& prev.labels.fileNotPreviewable === next.labels.fileNotPreviewable
|
||||||
|
&& prev.labels.goodReply === next.labels.goodReply
|
||||||
|
&& prev.labels.previewTitle === next.labels.previewTitle
|
||||||
|
&& prev.labels.quoteReply === next.labels.quoteReply
|
||||||
|
&& prev.labels.quotedReplyLabel === next.labels.quotedReplyLabel
|
||||||
|
&& prev.labels.user === next.labels.user
|
||||||
|
&& prev.labels.you === next.labels.you
|
||||||
|
&& prev.item.id === next.item.id
|
||||||
|
&& prev.item.role === next.item.role
|
||||||
|
&& prev.item.text === next.item.text
|
||||||
|
&& prev.item.quoted_reply === next.item.quoted_reply
|
||||||
|
&& prev.item.ts === next.item.ts
|
||||||
|
&& prev.item.kind === next.item.kind
|
||||||
|
&& prev.item.feedback === next.item.feedback
|
||||||
|
&& sameAttachments(prev.item.attachments, next.item.attachments)
|
||||||
|
));
|
||||||
|
|
||||||
export function DashboardConversationMessages({
|
export function DashboardConversationMessages({
|
||||||
conversation,
|
conversation,
|
||||||
isZh,
|
isZh,
|
||||||
|
|
@ -89,202 +368,34 @@ export function DashboardConversationMessages({
|
||||||
<>
|
<>
|
||||||
{conversation.map((item, idx) => {
|
{conversation.map((item, idx) => {
|
||||||
const itemKey = getConversationItemKey(item, idx);
|
const itemKey = getConversationItemKey(item, idx);
|
||||||
const isProgressBubble = item.role !== 'user' && (item.kind || 'final') === 'progress';
|
|
||||||
const isUserBubble = item.role === 'user';
|
|
||||||
const fullText = String(item.text || '');
|
|
||||||
const summaryText = isProgressBubble ? summarizeProgressText(fullText, isZh) : fullText;
|
|
||||||
const hasSummary = isProgressBubble && summaryText.trim().length > 0 && summaryText.trim() !== fullText.trim();
|
|
||||||
const progressCollapsible = isProgressBubble && (hasSummary || shouldCollapseProgress(fullText));
|
|
||||||
const normalizedUserText = isUserBubble ? normalizeUserMessageText(fullText) : '';
|
|
||||||
const userLineCount = isUserBubble ? normalizedUserText.split('\n').length : 0;
|
|
||||||
const userCollapsible = isUserBubble && userLineCount > 5;
|
|
||||||
const collapsible = isProgressBubble ? progressCollapsible : userCollapsible;
|
|
||||||
const expanded = isProgressBubble ? Boolean(expandedProgressByKey[itemKey]) : Boolean(expandedUserByKey[itemKey]);
|
|
||||||
const displayText = isProgressBubble && !expanded ? summaryText : fullText;
|
|
||||||
const currentDayKey = new Date(item.ts).toDateString();
|
const currentDayKey = new Date(item.ts).toDateString();
|
||||||
const prevDayKey = idx > 0 ? new Date(conversation[idx - 1].ts).toDateString() : '';
|
const prevDayKey = idx > 0 ? new Date(conversation[idx - 1].ts).toDateString() : '';
|
||||||
const showDateDivider = idx === 0 || currentDayKey !== prevDayKey;
|
const showDateDivider = idx === 0 || currentDayKey !== prevDayKey;
|
||||||
const isDeleting = Boolean(item.id && deletingMessageIdMap[item.id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DashboardConversationMessageRow
|
||||||
key={itemKey}
|
key={itemKey}
|
||||||
data-chat-message-id={item.id ? String(item.id) : undefined}
|
item={item}
|
||||||
>
|
itemKey={itemKey}
|
||||||
{showDateDivider ? (
|
isZh={isZh}
|
||||||
<div className="ops-chat-date-divider" aria-label={formatConversationDate(item.ts, isZh)}>
|
labels={labels}
|
||||||
<span>{formatConversationDate(item.ts, isZh)}</span>
|
showDateDivider={showDateDivider}
|
||||||
</div>
|
expandedProgress={Boolean(expandedProgressByKey[itemKey])}
|
||||||
) : null}
|
expandedUser={Boolean(expandedUserByKey[itemKey])}
|
||||||
<div className={`ops-chat-row ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
isDeleting={Boolean(item.id && deletingMessageIdMap[item.id])}
|
||||||
<div className={`ops-chat-item ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
isFeedbackSaving={Boolean(item.id && feedbackSavingByMessageId[item.id])}
|
||||||
{item.role !== 'user' && (
|
markdownComponents={markdownComponents}
|
||||||
<div className="ops-avatar bot" title="Nanobot">
|
workspaceDownloadExtensionSet={workspaceDownloadExtensionSet}
|
||||||
<img src={nanobotLogo} alt="Nanobot" />
|
onToggleProgressExpand={onToggleProgressExpand}
|
||||||
</div>
|
onToggleUserExpand={onToggleUserExpand}
|
||||||
)}
|
onEditUserPrompt={onEditUserPrompt}
|
||||||
{item.role === 'user' ? (
|
onCopyUserPrompt={onCopyUserPrompt}
|
||||||
<div className="ops-chat-hover-actions ops-chat-hover-actions-user">
|
onDeleteConversationMessage={onDeleteConversationMessage}
|
||||||
<LucentIconButton
|
onOpenWorkspacePath={onOpenWorkspacePath}
|
||||||
className="ops-chat-inline-action"
|
onSubmitAssistantFeedback={onSubmitAssistantFeedback}
|
||||||
onClick={() => onEditUserPrompt(item.text)}
|
onQuoteAssistantReply={onQuoteAssistantReply}
|
||||||
tooltip={labels.editPrompt}
|
onCopyAssistantReply={onCopyAssistantReply}
|
||||||
aria-label={labels.editPrompt}
|
/>
|
||||||
>
|
|
||||||
<Pencil size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-inline-action"
|
|
||||||
onClick={() => void onCopyUserPrompt(item.text)}
|
|
||||||
tooltip={labels.copyPrompt}
|
|
||||||
aria-label={labels.copyPrompt}
|
|
||||||
>
|
|
||||||
<Copy size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-inline-action"
|
|
||||||
onClick={() => void onDeleteConversationMessage(item)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
tooltip={labels.deleteMessage}
|
|
||||||
aria-label={labels.deleteMessage}
|
|
||||||
>
|
|
||||||
<Trash2 size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={`ops-chat-bubble ${item.role === 'user' ? 'user' : 'assistant'} ${(item.kind || 'final') === 'progress' ? 'progress' : ''}`}>
|
|
||||||
<div className="ops-chat-meta">
|
|
||||||
<span>{item.role === 'user' ? labels.you : 'Nanobot'}</span>
|
|
||||||
<div className="ops-chat-meta-right">
|
|
||||||
<span className="mono">{formatClock(item.ts)}</span>
|
|
||||||
{collapsible ? (
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-expand-icon-btn"
|
|
||||||
onClick={() => {
|
|
||||||
if (isProgressBubble) {
|
|
||||||
onToggleProgressExpand(itemKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onToggleUserExpand(itemKey);
|
|
||||||
}}
|
|
||||||
tooltip={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
|
||||||
aria-label={expanded ? (isZh ? '收起' : 'Collapse') : (isZh ? '展开' : 'Expand')}
|
|
||||||
>
|
|
||||||
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
||||||
</LucentIconButton>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`ops-chat-text ${collapsible && !expanded ? (isUserBubble ? 'is-collapsed-user' : 'is-collapsed') : ''}`}>
|
|
||||||
{item.text ? (
|
|
||||||
item.role === 'user' ? (
|
|
||||||
<>
|
|
||||||
{item.quoted_reply ? (
|
|
||||||
<div className="ops-user-quoted-reply">
|
|
||||||
<div className="ops-user-quoted-label">{labels.quotedReplyLabel}</div>
|
|
||||||
<div className="ops-user-quoted-text">{normalizeAssistantMessageText(item.quoted_reply)}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="whitespace-pre-wrap">{normalizeUserMessageText(displayText)}</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
|
||||||
components={markdownComponents}
|
|
||||||
>
|
|
||||||
{decorateWorkspacePathsForMarkdown(displayText)}
|
|
||||||
</ReactMarkdown>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{(item.attachments || []).length > 0 ? (
|
|
||||||
<div className="ops-chat-attachments">
|
|
||||||
{(item.attachments || []).map((rawPath) => {
|
|
||||||
const filePath = normalizeDashboardAttachmentPath(rawPath);
|
|
||||||
const fileAction = workspaceFileAction(filePath, workspaceDownloadExtensionSet);
|
|
||||||
const filename = filePath.split('/').pop() || filePath;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={`${item.ts}-${filePath}`}
|
|
||||||
className="ops-attach-link mono"
|
|
||||||
href="#"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
void onOpenWorkspacePath(filePath);
|
|
||||||
}}
|
|
||||||
title={fileAction === 'download' ? labels.download : fileAction === 'preview' ? labels.previewTitle : labels.fileNotPreviewable}
|
|
||||||
>
|
|
||||||
{fileAction === 'download' ? (
|
|
||||||
<Download size={12} className="ops-attach-link-icon" />
|
|
||||||
) : fileAction === 'preview' ? (
|
|
||||||
<Eye size={12} className="ops-attach-link-icon" />
|
|
||||||
) : (
|
|
||||||
<FileText size={12} className="ops-attach-link-icon" />
|
|
||||||
)}
|
|
||||||
<span className="ops-attach-link-name">{filename}</span>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{item.role === 'assistant' && !isProgressBubble ? (
|
|
||||||
<div className="ops-chat-reply-actions">
|
|
||||||
<LucentIconButton
|
|
||||||
className={`ops-chat-inline-action ${item.feedback === 'up' ? 'active-up' : ''}`}
|
|
||||||
onClick={() => void onSubmitAssistantFeedback(item, 'up')}
|
|
||||||
disabled={Boolean(item.id && feedbackSavingByMessageId[item.id])}
|
|
||||||
tooltip={labels.goodReply}
|
|
||||||
aria-label={labels.goodReply}
|
|
||||||
>
|
|
||||||
<ThumbsUp size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className={`ops-chat-inline-action ${item.feedback === 'down' ? 'active-down' : ''}`}
|
|
||||||
onClick={() => void onSubmitAssistantFeedback(item, 'down')}
|
|
||||||
disabled={Boolean(item.id && feedbackSavingByMessageId[item.id])}
|
|
||||||
tooltip={labels.badReply}
|
|
||||||
aria-label={labels.badReply}
|
|
||||||
>
|
|
||||||
<ThumbsDown size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-inline-action"
|
|
||||||
onClick={() => onQuoteAssistantReply(item)}
|
|
||||||
tooltip={labels.quoteReply}
|
|
||||||
aria-label={labels.quoteReply}
|
|
||||||
>
|
|
||||||
<Reply size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-inline-action"
|
|
||||||
onClick={() => void onCopyAssistantReply(item.text)}
|
|
||||||
tooltip={labels.copyReply}
|
|
||||||
aria-label={labels.copyReply}
|
|
||||||
>
|
|
||||||
<Copy size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
<LucentIconButton
|
|
||||||
className="ops-chat-inline-action"
|
|
||||||
onClick={() => void onDeleteConversationMessage(item)}
|
|
||||||
disabled={isDeleting}
|
|
||||||
tooltip={labels.deleteMessage}
|
|
||||||
aria-label={labels.deleteMessage}
|
|
||||||
>
|
|
||||||
<Trash2 size={13} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{item.role === 'user' && (
|
|
||||||
<div className="ops-avatar user" title={labels.user}>
|
|
||||||
<UserRound size={18} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue