mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* chore(lint): fix no-direct-set-state-in-use-effect violations Replaced 56 setState-in-useEffect anti-patterns across 21 files with idiomatic React patterns: derived state via useMemo, useSyncExternalStore for external stores, key-prop resets, render-phase prev-value trackers, and lazy useState initializers. Deleted dead defensive syncs in PlusSettings, ModelEditDialog, and the fallback-notice effect in CustomCommandChatModal. Narrowly suppressed three legitimate cases (interval-driven animation, DOM measurement, Lexical pill→context absorb). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * address codex comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
import { cn } from "@/lib/utils";
|
|
import { ReasoningStatus } from "@/LLMProviders/chainRunner/utils/AgentReasoningState";
|
|
import { ChevronRight } from "lucide-react";
|
|
import React, { useState } from "react";
|
|
|
|
/**
|
|
* Props for the AgentReasoningBlock component
|
|
*/
|
|
interface AgentReasoningBlockProps {
|
|
status: ReasoningStatus;
|
|
elapsedSeconds: number;
|
|
steps: string[];
|
|
isStreaming: boolean;
|
|
}
|
|
|
|
/**
|
|
* Formats elapsed time into a human-readable string
|
|
*
|
|
* @param seconds - Elapsed time in seconds
|
|
* @returns Formatted time string (e.g., "9s" or "1m 30s")
|
|
*/
|
|
const formatTime = (seconds: number): string => {
|
|
if (seconds < 60) {
|
|
return `${seconds}s`;
|
|
}
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}m ${secs}s`;
|
|
};
|
|
|
|
/**
|
|
* Animated spinner using a 7-dot sigma (Σ) pattern.
|
|
* Dots light up in sequence with gradient trail (snake effect).
|
|
*
|
|
* Grid positions (3x3 grid, sigma shape):
|
|
* 0 1 2
|
|
* 4
|
|
* 6 7 8
|
|
*
|
|
* Animation sequence (traces sigma shape):
|
|
* 2 → 1 → 0 → 4 → 6 → 7 → 8 → (all dim) → repeat
|
|
*/
|
|
const CopilotSpinner: React.FC = () => {
|
|
// Sigma pattern dots: [row, col, animation index]
|
|
// Animation traces the sigma: top-right to top-left, down to center, then bottom-left to bottom-right
|
|
const sigmaDots: { row: number; col: number; animIndex: number }[] = [
|
|
{ row: 0, col: 0, animIndex: 2 }, // top-left - 3rd
|
|
{ row: 0, col: 1, animIndex: 1 }, // top-center - 2nd
|
|
{ row: 0, col: 2, animIndex: 0 }, // top-right - 1st (leads)
|
|
{ row: 1, col: 1, animIndex: 3 }, // center - 4th
|
|
{ row: 2, col: 0, animIndex: 4 }, // bottom-left - 5th
|
|
{ row: 2, col: 1, animIndex: 5 }, // bottom-center - 6th
|
|
{ row: 2, col: 2, animIndex: 6 }, // bottom-right - 7th (last)
|
|
];
|
|
|
|
const dotSize = 2.5;
|
|
const gap = 3;
|
|
const gridSize = dotSize * 3 + gap * 2;
|
|
|
|
return (
|
|
<svg
|
|
width={gridSize}
|
|
height={gridSize}
|
|
viewBox={`0 0 ${gridSize} ${gridSize}`}
|
|
className="copilot-spinner"
|
|
>
|
|
{sigmaDots.map((dot) => {
|
|
const cx = dot.col * (dotSize + gap) + dotSize / 2;
|
|
const cy = dot.row * (dotSize + gap) + dotSize / 2;
|
|
|
|
return (
|
|
<circle
|
|
key={`${dot.row}-${dot.col}`}
|
|
cx={cx}
|
|
cy={cy}
|
|
r={dotSize / 2}
|
|
// eslint-disable-next-line tailwindcss/no-custom-classname
|
|
className={`copilot-spinner-dot copilot-spinner-dot-${dot.animIndex}`}
|
|
/>
|
|
);
|
|
})}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* AgentReasoningBlock - Displays the agent's reasoning process
|
|
*
|
|
* This component replaces the old tool call banner with a more informative
|
|
* reasoning display that shows:
|
|
* - Active spinner during reasoning
|
|
* - Elapsed time counter
|
|
* - Current reasoning steps (last 2)
|
|
* - Collapsible view after completion
|
|
*
|
|
* States:
|
|
* - reasoning: Expanded, showing steps and active spinner
|
|
* - collapsed: Collapsed after reasoning, before response
|
|
* - complete: Response done, expandable to see steps
|
|
*/
|
|
export const AgentReasoningBlock: React.FC<AgentReasoningBlockProps> = ({
|
|
status,
|
|
elapsedSeconds,
|
|
steps,
|
|
isStreaming,
|
|
}) => {
|
|
const [isExpanded, setIsExpanded] = useState(status === "reasoning");
|
|
const [prevStatus, setPrevStatus] = useState(status);
|
|
|
|
if (status !== prevStatus) {
|
|
setPrevStatus(status);
|
|
if (status === "reasoning") {
|
|
setIsExpanded(true);
|
|
} else if (status === "collapsed" || status === "complete") {
|
|
setIsExpanded(false);
|
|
}
|
|
}
|
|
|
|
// Don't render anything if idle
|
|
if (status === "idle") {
|
|
return null;
|
|
}
|
|
|
|
const isActive = status === "reasoning";
|
|
const canExpand = !isActive && steps.length > 0;
|
|
|
|
return (
|
|
<Collapsible
|
|
open={canExpand ? isExpanded : isActive}
|
|
onOpenChange={canExpand ? setIsExpanded : undefined}
|
|
disabled={!canExpand}
|
|
className="agent-reasoning-block"
|
|
>
|
|
<CollapsibleTrigger asChild disabled={!canExpand}>
|
|
<div
|
|
className={cn(
|
|
"agent-reasoning-header",
|
|
canExpand && "tw-cursor-pointer",
|
|
!canExpand && "tw-cursor-default"
|
|
)}
|
|
>
|
|
{/* Spinner or expand chevron */}
|
|
<span className="agent-reasoning-icon">
|
|
{isActive ? (
|
|
<CopilotSpinner />
|
|
) : (
|
|
<ChevronRight
|
|
className={cn(
|
|
"tw-size-3 tw-text-muted tw-transition-transform",
|
|
isExpanded && "tw-rotate-90"
|
|
)}
|
|
/>
|
|
)}
|
|
</span>
|
|
|
|
{/* Title and timer */}
|
|
<span className="agent-reasoning-title">{isActive ? "Reasoning" : "Thought for"}</span>
|
|
<span className="agent-reasoning-timer">{formatTime(elapsedSeconds)}</span>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
{/* Steps - visible when expanded or actively reasoning */}
|
|
<CollapsibleContent>
|
|
{steps.length > 0 && (
|
|
<ul className="agent-reasoning-steps">
|
|
{steps.map((step, i) => (
|
|
// eslint-disable-next-line @eslint-react/no-array-index-key -- steps are append-only with no stable id; text may repeat
|
|
<li key={i} className="agent-reasoning-step">
|
|
{step}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
};
|
|
|
|
export default AgentReasoningBlock;
|