agent-ecosystem/src/renderer/components/team/dialogs/TaskCommentsSection.tsx
iliya 0e85c1f31a feat: enhance task log retrieval with optional parameters
- Updated the getLogsForTask function to accept optional parameters for owner and status, allowing for more granular log retrieval based on task state.
- Modified the TeamMemberLogsFinder to include owner's session logs when the task is in progress, improving visibility into ongoing activities.
- Introduced new UI components for better task tracking, including ActiveTasksBlock and ProvisioningProgressBlock, enhancing user experience during team provisioning.
- Refactored related components to support the new functionality, ensuring seamless integration across the application.
2026-02-23 14:56:31 +02:00

244 lines
9.4 KiB
TypeScript

import { useCallback, useMemo, useState } from 'react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { ReplyQuoteBlock } from '@renderer/components/team/activity/ReplyQuoteBlock';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useMarkCommentsRead } from '@renderer/hooks/useMarkCommentsRead';
import { useStore } from '@renderer/store';
import { buildReplyBlock, parseMessageReply } from '@renderer/utils/agentMessageFormatting';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { getModifierKeyName } from '@renderer/utils/keyboardUtils';
import { formatDistanceToNow } from 'date-fns';
import { ChevronDown, ChevronUp, MessageSquare, Reply, Send, X } from 'lucide-react';
import type { MentionSuggestion } from '@renderer/types/mention';
import type { ResolvedTeamMember, TaskComment } from '@shared/types';
const MAX_COMMENT_LENGTH = 2000;
interface TaskCommentsSectionProps {
teamName: string;
taskId: string;
comments: TaskComment[];
members: ResolvedTeamMember[];
}
export const TaskCommentsSection = ({
teamName,
taskId,
comments,
members,
}: TaskCommentsSectionProps): React.JSX.Element => {
const addTaskComment = useStore((s) => s.addTaskComment);
const addingComment = useStore((s) => s.addingComment);
const commentsRef = useMarkCommentsRead(teamName, taskId, comments);
const [replyTo, setReplyTo] = useState<{ author: string; text: string } | null>(null);
const [expandedCommentIds, setExpandedCommentIds] = useState<Set<string>>(new Set());
const toggleCommentExpanded = useCallback((commentId: string) => {
setExpandedCommentIds((prev) => {
const next = new Set(prev);
if (next.has(commentId)) next.delete(commentId);
else next.add(commentId);
return next;
});
}, []);
const draft = useDraftPersistence({ key: `taskComment:${teamName}:${taskId}` });
const mentionSuggestions = useMemo<MentionSuggestion[]>(
() =>
members.map((m) => ({
id: m.name,
name: m.name,
subtitle: formatAgentRole(m.role) ?? formatAgentRole(m.agentType) ?? undefined,
color: m.color,
})),
[members]
);
const trimmed = draft.value.trim();
const remaining = MAX_COMMENT_LENGTH - trimmed.length;
const canSubmit = trimmed.length > 0 && trimmed.length <= MAX_COMMENT_LENGTH && !addingComment;
const handleSubmit = useCallback(async () => {
if (!canSubmit) return;
try {
const text = replyTo ? buildReplyBlock(replyTo.author, replyTo.text, trimmed) : trimmed;
await addTaskComment(teamName, taskId, text);
draft.clearDraft();
setReplyTo(null);
} catch {
// Error is stored in addCommentError via store
}
}, [canSubmit, addTaskComment, teamName, taskId, trimmed, draft, replyTo]);
return (
<div ref={commentsRef}>
<div className="mb-2 flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)]">
<MessageSquare size={12} />
Comments
{comments.length > 0 ? (
<span className="rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0 text-[10px]">
{comments.length}
</span>
) : null}
</div>
{comments.length > 0 ? (
<div className="mb-3 space-y-2">
{comments.map((comment) => (
<div
key={comment.id}
className="group rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-2.5"
>
<div className="mb-1 flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
<span
className="font-medium"
style={{
color:
comment.author === 'user'
? 'var(--color-text-secondary)'
: (members.find((m) => m.name === comment.author)?.color ??
'var(--color-text-secondary)'),
}}
>
{comment.author}
</span>
<span>
{(() => {
const date = new Date(comment.createdAt);
return isNaN(date.getTime())
? 'unknown time'
: formatDistanceToNow(date, { addSuffix: true });
})()}
</span>
<button
type="button"
className="flex items-center gap-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:text-[var(--color-text-secondary)] group-hover:opacity-100"
onClick={() => toggleCommentExpanded(comment.id)}
title={expandedCommentIds.has(comment.id) ? 'Свернуть' : 'Развернуть'}
>
{expandedCommentIds.has(comment.id) ? (
<ChevronUp size={12} />
) : (
<ChevronDown size={12} />
)}
</button>
<button
type="button"
className="ml-auto flex items-center gap-0.5 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:text-[var(--color-text-secondary)] group-hover:opacity-100"
onClick={() =>
setReplyTo({
author: comment.author,
text: parseMessageReply(comment.text)?.replyText ?? comment.text,
})
}
>
<Reply size={11} />
Reply
</button>
</div>
<div className="text-xs">
{(() => {
const reply = parseMessageReply(comment.text);
const expanded = expandedCommentIds.has(comment.id);
return reply ? (
<ReplyQuoteBlock
reply={reply}
bodyMaxHeight={expanded ? undefined : 'max-h-56'}
/>
) : (
<MarkdownViewer
content={comment.text}
maxHeight={expanded ? undefined : 'max-h-[120px]'}
/>
);
})()}
</div>
</div>
))}
</div>
) : null}
{replyTo ? (
<div className="mb-2 flex items-start gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-2">
<div className="min-w-0 flex-1">
<div className="mb-0.5 text-[10px] font-medium text-[var(--color-text-muted)]">
Replying to{' '}
<span
className="font-semibold"
style={{
color:
replyTo.author === 'user'
? 'var(--color-text-secondary)'
: (members.find((m) => m.name === replyTo.author)?.color ??
'var(--color-text-secondary)'),
}}
>
@{replyTo.author}
</span>
</div>
<div className="line-clamp-3 text-[11px] text-[var(--color-text-muted)]">
{replyTo.text}
</div>
</div>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="shrink-0 rounded p-0.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text-secondary)]"
onClick={() => setReplyTo(null)}
>
<X size={12} />
</button>
</TooltipTrigger>
<TooltipContent side="left">Cancel reply</TooltipContent>
</Tooltip>
</div>
) : null}
<div className="relative">
<MentionableTextarea
id={`task-comment-${taskId}`}
placeholder={`Add a comment... (${getModifierKeyName()}+Enter to send)`}
value={draft.value}
onValueChange={draft.setValue}
suggestions={mentionSuggestions}
minRows={2}
maxRows={8}
maxLength={MAX_COMMENT_LENGTH}
disabled={addingComment}
cornerAction={
<button
type="button"
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-blue-600 px-3 py-1.5 text-[11px] font-medium text-white shadow-sm transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!canSubmit}
onClick={() => void handleSubmit()}
>
<Send size={12} />
Comment
</button>
}
footerRight={
<div className="flex items-center gap-2">
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{draft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Draft saved</span>
) : null}
</div>
}
/>
</div>
</div>
);
};