mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-05-03 13:30:47 +00:00
fix: lint
(cherry picked from commit 2aac8ff0563483d01b16bb5ee8c191fafa8aff14)
This commit is contained in:
@@ -147,8 +147,8 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => {
|
||||
);
|
||||
|
||||
// Optimized helper function to process text nodes recursively
|
||||
const processTextNode = useCallback(
|
||||
(nodeChildren: any): any => {
|
||||
const processTextNode = useMemo(() => {
|
||||
const fn = (nodeChildren: any): any => {
|
||||
if (!processedSearch) {
|
||||
return nodeChildren;
|
||||
}
|
||||
@@ -163,15 +163,13 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => {
|
||||
return createHighlightedText(child);
|
||||
}
|
||||
|
||||
// Avoid deep cloning React elements to prevent memory leaks
|
||||
// Only process if it's a simple object with props
|
||||
if (child && typeof child === 'object' && child.props && child.props.children !== undefined) {
|
||||
return {
|
||||
...child,
|
||||
key: child.key || `processed-${index}`,
|
||||
props: {
|
||||
...child.props,
|
||||
children: processTextNode(child.props.children),
|
||||
children: fn(child.props.children),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -180,7 +178,6 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle React elements safely
|
||||
if (
|
||||
nodeChildren &&
|
||||
typeof nodeChildren === 'object' &&
|
||||
@@ -191,25 +188,31 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => {
|
||||
...nodeChildren,
|
||||
props: {
|
||||
...nodeChildren.props,
|
||||
children: processTextNode(nodeChildren.props.children),
|
||||
children: fn(nodeChildren.props.children),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return nodeChildren;
|
||||
},
|
||||
[processedSearch, createHighlightedText],
|
||||
);
|
||||
};
|
||||
|
||||
return fn;
|
||||
}, [processedSearch, createHighlightedText]);
|
||||
|
||||
// Create a simple component renderer factory to avoid recreating functions
|
||||
const createComponentRenderer = useCallback(
|
||||
(ComponentName: string) => {
|
||||
return ({ children: nodeChildren, ...props }: any) => {
|
||||
const Component = ComponentName as React.ElementType;
|
||||
|
||||
const Renderer = ({ children: nodeChildren, ...props }: Record<string, unknown>) => {
|
||||
const processedChildren = processTextNode(nodeChildren);
|
||||
const Component = ComponentName as any;
|
||||
|
||||
return <Component {...props}>{processedChildren}</Component>;
|
||||
};
|
||||
|
||||
Renderer.displayName = `Highlighted(${ComponentName})`;
|
||||
|
||||
return Renderer;
|
||||
},
|
||||
[processTextNode],
|
||||
);
|
||||
|
||||
@@ -67,7 +67,11 @@ const injectedColorClasses = new Set<string>();
|
||||
const rgbStringToHex = (rgb: string): string =>
|
||||
rgb
|
||||
.split(',')
|
||||
.map((part) => Math.min(255, Math.max(0, parseInt(part.trim(), 10))).toString(16).padStart(2, '0'))
|
||||
.map((part) =>
|
||||
Math.min(255, Math.max(0, parseInt(part.trim(), 10)))
|
||||
.toString(16)
|
||||
.padStart(2, '0'),
|
||||
)
|
||||
.join('');
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,10 +102,7 @@ export function useXterm({ theme }: { theme: 'dark' | 'light' | 'system' }): Use
|
||||
const openLink = (event: MouseEvent, uri: string) => {
|
||||
const uriLower = uri.toLowerCase();
|
||||
|
||||
if (
|
||||
(mac ? event.metaKey : event.ctrlKey) &&
|
||||
SAFE_PROTOCOLS.some((p) => uriLower.startsWith(p))
|
||||
) {
|
||||
if ((mac ? event.metaKey : event.ctrlKey) && SAFE_PROTOCOLS.some((p) => uriLower.startsWith(p))) {
|
||||
window.open(uri, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,7 +121,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
function InputGroupTextarea({ className, ...props }: React.ComponentProps<typeof Textarea>) {
|
||||
return (
|
||||
<Textarea
|
||||
className={cn(
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
|
||||
"[&_svg:not([class*='size-'])]:size-3",
|
||||
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
|
||||
return (
|
||||
<kbd
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
|
||||
"[&_svg:not([class*='size-'])]:size-3",
|
||||
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
|
||||
className,
|
||||
)}
|
||||
data-slot="kbd"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn("inline-flex items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<kbd
|
||||
className={cn('inline-flex items-center gap-1', className)}
|
||||
data-slot="kbd-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
export { Kbd, KbdGroup };
|
||||
|
||||
@@ -16,7 +16,7 @@ const useTextarea = ({
|
||||
textareaRef,
|
||||
triggerAutoSize,
|
||||
}: UseTextareaProps) => {
|
||||
const [init, setInit] = React.useState(true);
|
||||
const initRef = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
const offsetBorder = 0;
|
||||
@@ -26,20 +26,20 @@ const useTextarea = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (init) {
|
||||
if (initRef.current) {
|
||||
textareaElement.style.minHeight = `${minHeight + offsetBorder}px`;
|
||||
|
||||
if (maxHeight > minHeight) {
|
||||
textareaElement.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
|
||||
setInit(false);
|
||||
initRef.current = false;
|
||||
}
|
||||
|
||||
textareaElement.style.height = `${minHeight + offsetBorder}px`;
|
||||
const scrollHeight = textareaElement.scrollHeight;
|
||||
textareaElement.style.height = scrollHeight > maxHeight ? `${maxHeight}px` : `${scrollHeight + offsetBorder}px`;
|
||||
}, [textareaRef.current, triggerAutoSize]);
|
||||
}, [triggerAutoSize, maxHeight, minHeight, textareaRef]);
|
||||
};
|
||||
|
||||
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Copy } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { AgentLogFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -45,20 +45,23 @@ const FlowAgent = ({ log, searchValue = '' }: FlowAgentProps) => {
|
||||
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||
|
||||
// Auto-expand details if they contain search matches
|
||||
useEffect(() => {
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasResultMatch, setPrevHasResultMatch] = useState(searchChecks.hasResultMatch);
|
||||
|
||||
if (searchValue !== prevSearchValue || searchChecks.hasResultMatch !== prevHasResultMatch) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasResultMatch(searchChecks.hasResultMatch);
|
||||
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand result block only if it contains the search term
|
||||
if (searchChecks.hasResultMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasResultMatch]);
|
||||
}
|
||||
|
||||
// Determine if we should show full task or preview
|
||||
// Show full task if: search found in task OR details are manually visible OR task is short
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Copy } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { AssistantLogFragmentFragment, MessageLogFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -48,26 +48,34 @@ const FlowMessage = ({ log, searchValue = '' }: FlowMessageProps) => {
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(isReportMessage);
|
||||
const [isThinkingVisible, setIsThinkingVisible] = useState(false);
|
||||
|
||||
// Auto-expand blocks if they contain search matches
|
||||
useEffect(() => {
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasThinkingMatch, setPrevHasThinkingMatch] = useState(searchChecks.hasThinkingMatch);
|
||||
const [prevHasResultMatch, setPrevHasResultMatch] = useState(searchChecks.hasResultMatch);
|
||||
|
||||
if (
|
||||
searchValue !== prevSearchValue ||
|
||||
searchChecks.hasThinkingMatch !== prevHasThinkingMatch ||
|
||||
searchChecks.hasResultMatch !== prevHasResultMatch
|
||||
) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasThinkingMatch(searchChecks.hasThinkingMatch);
|
||||
setPrevHasResultMatch(searchChecks.hasResultMatch);
|
||||
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand thinking block only if it contains the search term
|
||||
if (searchChecks.hasThinkingMatch) {
|
||||
setIsThinkingVisible(true);
|
||||
}
|
||||
|
||||
// Expand result block only if it contains the search term
|
||||
if (searchChecks.hasResultMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(isReportMessage);
|
||||
setIsThinkingVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasThinkingMatch, searchChecks.hasResultMatch, isReportMessage]);
|
||||
}
|
||||
|
||||
// Use useCallback to memoize the toggle functions
|
||||
const toggleDetails = useCallback(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ListCheck, ListTodo } from 'lucide-react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import type { SubtaskFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -41,20 +41,27 @@ const FlowSubtask = ({ searchValue = '', subtask }: FlowSubtaskProps) => {
|
||||
};
|
||||
}, [searchValue, description, result]);
|
||||
|
||||
// Auto-expand details if they contain search matches
|
||||
useEffect(() => {
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasMatch, setPrevHasMatch] = useState(
|
||||
searchChecks.hasDescriptionMatch || searchChecks.hasResultMatch,
|
||||
);
|
||||
|
||||
const hasMatch = searchChecks.hasDescriptionMatch || searchChecks.hasResultMatch;
|
||||
|
||||
if (searchValue !== prevSearchValue || hasMatch !== prevHasMatch) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasMatch(hasMatch);
|
||||
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand details if description or result contains the search term
|
||||
if (searchChecks.hasDescriptionMatch || searchChecks.hasResultMatch) {
|
||||
if (hasMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasDescriptionMatch, searchChecks.hasResultMatch]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative flex gap-2.5 pb-4 pl-0.5">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import type { TaskFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -41,20 +41,23 @@ const FlowTask = ({ searchValue = '', task }: FlowTaskProps) => {
|
||||
};
|
||||
}, [searchValue, result]);
|
||||
|
||||
// Auto-expand details if they contain search matches
|
||||
useEffect(() => {
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasResultMatch, setPrevHasResultMatch] = useState(searchChecks.hasResultMatch);
|
||||
|
||||
if (searchValue !== prevSearchValue || searchChecks.hasResultMatch !== prevHasResultMatch) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasResultMatch(searchChecks.hasResultMatch);
|
||||
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand result block only if it contains the search term
|
||||
if (searchChecks.hasResultMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasResultMatch]);
|
||||
}
|
||||
|
||||
const sortedSubtasks = [...(subtasks || [])].sort((a, b) => +a.id - +b.id);
|
||||
const hasSubtasks = subtasks && subtasks.length > 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Copy, Hammer } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { SearchLogFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -41,21 +41,23 @@ const FlowTool = ({ log, searchValue = '' }: FlowToolProps) => {
|
||||
}, [searchValue, query, result]);
|
||||
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasResultMatch, setPrevHasResultMatch] = useState(searchChecks.hasResultMatch);
|
||||
|
||||
if (searchValue !== prevSearchValue || searchChecks.hasResultMatch !== prevHasResultMatch) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasResultMatch(searchChecks.hasResultMatch);
|
||||
|
||||
// Auto-expand details if they contain search matches
|
||||
useEffect(() => {
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand result block only if it contains the search term
|
||||
if (searchChecks.hasResultMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasResultMatch]);
|
||||
}
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
await copyMessageToClipboard({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Copy } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { VectorStoreLogFragmentFragment } from '@/graphql/types';
|
||||
|
||||
@@ -85,21 +85,23 @@ const FlowVectorStore = ({ log, searchValue = '' }: FlowVectorStoreProps) => {
|
||||
}, [searchValue, query, result]);
|
||||
|
||||
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
|
||||
const [prevSearchValue, setPrevSearchValue] = useState(searchValue);
|
||||
const [prevHasResultMatch, setPrevHasResultMatch] = useState(searchChecks.hasResultMatch);
|
||||
|
||||
if (searchValue !== prevSearchValue || searchChecks.hasResultMatch !== prevHasResultMatch) {
|
||||
setPrevSearchValue(searchValue);
|
||||
setPrevHasResultMatch(searchChecks.hasResultMatch);
|
||||
|
||||
// Auto-expand details if they contain search matches
|
||||
useEffect(() => {
|
||||
const trimmedSearch = searchValue.trim();
|
||||
|
||||
if (trimmedSearch) {
|
||||
// Expand result block only if it contains the search term
|
||||
if (searchChecks.hasResultMatch) {
|
||||
setIsDetailsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Reset to default state when search is cleared
|
||||
setIsDetailsVisible(false);
|
||||
}
|
||||
}, [searchValue, searchChecks.hasResultMatch]);
|
||||
}
|
||||
|
||||
const description = getDescription(log);
|
||||
|
||||
|
||||
@@ -82,9 +82,7 @@ export const useAdaptiveColumnVisibility = ({
|
||||
const userPreference = userPreferences[column.id];
|
||||
|
||||
const isVisible =
|
||||
userPreference !== undefined
|
||||
? !shouldHideByWidth && userPreference
|
||||
: !shouldHideByWidth;
|
||||
userPreference !== undefined ? !shouldHideByWidth && userPreference : !shouldHideByWidth;
|
||||
|
||||
return [column.id, isVisible];
|
||||
}),
|
||||
|
||||
@@ -287,9 +287,7 @@ const parseMarkdownTokens = (markdown: string): ParsedContent[] => {
|
||||
}
|
||||
|
||||
case 'list': {
|
||||
const tokenItems = (
|
||||
Array.isArray(token.items) ? token.items : []
|
||||
) as Array<Record<string, unknown>>;
|
||||
const tokenItems = (Array.isArray(token.items) ? token.items : []) as Array<Record<string, unknown>>;
|
||||
const items = tokenItems.map((item) => ({
|
||||
inlineTokens: parseInlineTokens(String(item.text || '')),
|
||||
raw: String(item.text || ''),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SortingState, VisibilityState } from '@tanstack/react-table';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const sortingSchema = z.array(z.object({ desc: z.boolean(), id: z.string() }));
|
||||
@@ -9,7 +10,7 @@ const pageStateSchema = z.object({ page: z.number(), pageSize: z.number() });
|
||||
|
||||
export type StoredPageState = z.infer<typeof pageStateSchema>;
|
||||
|
||||
function loadFromStorage<T>(key: string, schema: z.ZodType<T>): T | null {
|
||||
function loadFromStorage<T>(key: string, schema: z.ZodType<T>): null | T {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
|
||||
@@ -33,19 +34,14 @@ function saveToStorage(key: string, value: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
export const loadSorting = (key: string): SortingState | null => loadFromStorage(key, sortingSchema);
|
||||
export const loadSorting = (key: string): null | SortingState => loadFromStorage(key, sortingSchema);
|
||||
|
||||
export const loadColumnVisibility = (key: string): VisibilityState | null =>
|
||||
loadFromStorage(key, visibilitySchema);
|
||||
export const loadColumnVisibility = (key: string): null | VisibilityState => loadFromStorage(key, visibilitySchema);
|
||||
|
||||
export const loadPageState = (key: string): StoredPageState | null =>
|
||||
loadFromStorage(key, pageStateSchema);
|
||||
export const loadPageState = (key: string): null | StoredPageState => loadFromStorage(key, pageStateSchema);
|
||||
|
||||
export const saveSorting = (key: string, sorting: SortingState): void =>
|
||||
saveToStorage(key, sorting);
|
||||
export const saveSorting = (key: string, sorting: SortingState): void => saveToStorage(key, sorting);
|
||||
|
||||
export const saveColumnVisibility = (key: string, visibility: VisibilityState): void =>
|
||||
saveToStorage(key, visibility);
|
||||
export const saveColumnVisibility = (key: string, visibility: VisibilityState): void => saveToStorage(key, visibility);
|
||||
|
||||
export const savePageState = (key: string, state: StoredPageState): void =>
|
||||
saveToStorage(key, state);
|
||||
export const savePageState = (key: string, state: StoredPageState): void => saveToStorage(key, state);
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface CopyableMessage {
|
||||
* This removes ANSI escape codes and returns formatted text as it appears in UI
|
||||
*/
|
||||
export const getCleanTerminalText = (terminalContent: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
let hiddenTerminal: null | XTerminal = null;
|
||||
let hiddenDiv: HTMLDivElement | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
@@ -68,14 +68,6 @@ export const getCleanTerminalText = (terminalContent: string): Promise<string> =
|
||||
}
|
||||
};
|
||||
|
||||
const safeReject = (error: any) => {
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Create a hidden terminal instance
|
||||
hiddenTerminal = new XTerminal({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Logo from '@/components/icons/logo';
|
||||
@@ -7,6 +7,7 @@ import { useFlowReportQuery } from '@/graphql/types';
|
||||
import { Log } from '@/lib/log';
|
||||
import { generateFileName, generatePDFFromMarkdown, generateReport } from '@/lib/report';
|
||||
|
||||
type PdfPhase = 'done' | 'error' | 'idle';
|
||||
type ReportState = 'content' | 'error' | 'generating' | 'loading';
|
||||
|
||||
const FlowReport = () => {
|
||||
@@ -15,9 +16,17 @@ const FlowReport = () => {
|
||||
const download = searchParams.has('download');
|
||||
const silent = searchParams.has('silent');
|
||||
|
||||
const [state, setState] = useState<ReportState>('loading');
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const [reportContent, setReportContent] = useState<string>('');
|
||||
const [pdfPhase, setPdfPhase] = useState<PdfPhase>('idle');
|
||||
const [pdfError, setPdfError] = useState<null | string>(null);
|
||||
const pdfTriggered = useRef(false);
|
||||
|
||||
const [prevFlowId, setPrevFlowId] = useState(flowId);
|
||||
|
||||
if (flowId !== prevFlowId) {
|
||||
setPrevFlowId(flowId);
|
||||
setPdfPhase('idle');
|
||||
setPdfError(null);
|
||||
}
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -29,55 +38,58 @@ const FlowReport = () => {
|
||||
variables: { id: flowId! },
|
||||
});
|
||||
|
||||
// Reset state when component mounts or flowId changes
|
||||
const dataReady = !loading && !queryError && !!data?.flow;
|
||||
|
||||
const reportContent = useMemo(
|
||||
() => (dataReady ? generateReport(data.tasks || [], data.flow!) : ''),
|
||||
[dataReady, data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setState('loading');
|
||||
setError(null);
|
||||
setReportContent('');
|
||||
pdfTriggered.current = false;
|
||||
}, [flowId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
if (!dataReady || !download || pdfTriggered.current || !data?.flow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryError || !data?.flow) {
|
||||
setError('Failed to load flow data');
|
||||
setState('error');
|
||||
pdfTriggered.current = true;
|
||||
|
||||
return;
|
||||
}
|
||||
const fileName = `${generateFileName(data.flow)}.pdf`;
|
||||
|
||||
// Generate report content using flow and tasks from GraphQL response
|
||||
const content = generateReport(data.tasks || [], data.flow);
|
||||
setReportContent(content);
|
||||
generatePDFFromMarkdown(reportContent, fileName)
|
||||
.then(() => {
|
||||
if (silent) {
|
||||
setTimeout(() => window.close(), 1000);
|
||||
} else {
|
||||
setPdfPhase('done');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error('PDF generation failed:', err);
|
||||
setPdfError('Failed to generate PDF');
|
||||
setPdfPhase('error');
|
||||
});
|
||||
}, [dataReady, download, silent, reportContent, data]);
|
||||
|
||||
if (download) {
|
||||
// Download mode - generate PDF and download it
|
||||
setState('generating');
|
||||
const fileName = `${generateFileName(data.flow)}.pdf`;
|
||||
let state: ReportState;
|
||||
let errorMessage: null | string = null;
|
||||
|
||||
generatePDFFromMarkdown(content, fileName)
|
||||
.then(() => {
|
||||
if (silent) {
|
||||
// Silent download - close window after successful download
|
||||
setTimeout(() => window.close(), 1000);
|
||||
} else {
|
||||
// Normal download - show content after download
|
||||
setState('content');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error('PDF generation failed:', err);
|
||||
setError('Failed to generate PDF');
|
||||
setState('error');
|
||||
});
|
||||
} else {
|
||||
setState('content');
|
||||
}
|
||||
}, [data, loading, queryError, download, silent]);
|
||||
if (loading) {
|
||||
state = 'loading';
|
||||
} else if (queryError || !data?.flow) {
|
||||
state = 'error';
|
||||
errorMessage = 'Failed to load flow data';
|
||||
} else if (pdfPhase === 'error') {
|
||||
state = 'error';
|
||||
errorMessage = pdfError;
|
||||
} else if (download && pdfPhase !== 'done') {
|
||||
state = 'generating';
|
||||
} else {
|
||||
state = 'content';
|
||||
}
|
||||
|
||||
// Loading state (for all modes during initial loading and PDF generation)
|
||||
if (state === 'loading' || state === 'generating') {
|
||||
return (
|
||||
<div className="min-h-screen bg-linear-to-br from-blue-50 via-white to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||
@@ -99,7 +111,6 @@ const FlowReport = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (state === 'error') {
|
||||
return (
|
||||
<div className="min-h-screen bg-linear-to-br from-red-50 via-white to-orange-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
||||
@@ -108,7 +119,7 @@ const FlowReport = () => {
|
||||
<div className="flex flex-col gap-4 text-center">
|
||||
<h1 className="text-2xl font-semibold text-red-600 dark:text-red-400">Error Loading Report</h1>
|
||||
<p className="max-w-md text-gray-600 dark:text-gray-400">
|
||||
{error || 'An unexpected error occurred while loading the report.'}
|
||||
{errorMessage || 'An unexpected error occurred while loading the report.'}
|
||||
</p>
|
||||
<button
|
||||
className="mt-4 rounded-md bg-red-600 px-4 py-2 text-white transition-colors hover:bg-red-700"
|
||||
@@ -122,7 +133,6 @@ const FlowReport = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Content viewing state (normal mode without download)
|
||||
return (
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||
<div className="h-screen w-full overflow-auto p-8">
|
||||
|
||||
@@ -13,10 +13,7 @@ const Login = () => {
|
||||
const authProviders = authInfo?.providers || [];
|
||||
|
||||
// Extract the return URL from either location state or query parameters
|
||||
const returnUrl = getSafeReturnUrl(
|
||||
(location.state?.from as string) || searchParams.get('returnUrl'),
|
||||
'/flows/new',
|
||||
);
|
||||
const returnUrl = getSafeReturnUrl((location.state?.from as string) || searchParams.get('returnUrl'), '/flows/new');
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full items-center justify-center">
|
||||
|
||||
@@ -105,7 +105,6 @@ const formatFullDateTime = (dateString: string) => {
|
||||
const SettingsMcpServers = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
// Mocked data stored locally. This can be replaced by a real query later.
|
||||
const initialData: McpServerItem[] = useMemo(
|
||||
() => [
|
||||
|
||||
@@ -533,7 +533,6 @@ const SettingsPrompt = () => {
|
||||
|
||||
// For creation, check if the template is identical to the default
|
||||
if (!isUpdate && formData.template === promptInfo.defaultSystemTemplate) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,7 +584,6 @@ const SettingsPrompt = () => {
|
||||
|
||||
// For creation, check if the template is identical to the default
|
||||
if (!isUpdate && formData.template === promptInfo.defaultHumanTemplate) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ const SettingsPrompts = () => {
|
||||
type: 'all' | 'human' | 'system' | 'tool';
|
||||
}>(null);
|
||||
|
||||
|
||||
// Three-way sorting handler: null -> asc -> desc -> null
|
||||
const handleColumnSort = (column: {
|
||||
clearSorting: () => void;
|
||||
|
||||
@@ -328,7 +328,9 @@ const FormModelComboboxItem: React.FC<FormModelComboboxItemProps> = ({
|
||||
const displayValue = field.value ?? '';
|
||||
|
||||
// Format price for display
|
||||
const formatPrice = (price?: null | { cacheRead: number; cacheWrite: number; input: number; output: number }): string => {
|
||||
const formatPrice = (
|
||||
price?: null | { cacheRead: number; cacheWrite: number; input: number; output: number },
|
||||
): string => {
|
||||
if (!price || ((!price.input || price.input === 0) && (!price.output || price.output === 0))) {
|
||||
return 'free';
|
||||
}
|
||||
@@ -338,7 +340,7 @@ const FormModelComboboxItem: React.FC<FormModelComboboxItemProps> = ({
|
||||
};
|
||||
|
||||
const basePrice = `$${formatValue(price.input)}/$${formatValue(price.output)}`;
|
||||
|
||||
|
||||
// Add cache prices if available
|
||||
const hasCachePrices = (price.cacheRead && price.cacheRead > 0) || (price.cacheWrite && price.cacheWrite > 0);
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ const SettingsProviders = () => {
|
||||
const [deletingProvider, setDeletingProvider] = useState<null | Provider>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
// Get current page from URL
|
||||
const currentPage = useMemo(() => {
|
||||
const page = searchParams.get('page');
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ThemeProvider = ({
|
||||
// Store only light or dark themes
|
||||
localStorage.setItem(storageKey, theme);
|
||||
}
|
||||
|
||||
setTheme(theme);
|
||||
},
|
||||
theme,
|
||||
|
||||
@@ -286,8 +286,8 @@
|
||||
--ring: oklch(0.25 0.14 245);
|
||||
--chart-1: oklch(0.25 0.14 245);
|
||||
--chart-2: oklch(0.38 0.18 245);
|
||||
--chart-3: oklch(0.50 0.22 245);
|
||||
--chart-4: oklch(0.42 0.10 245);
|
||||
--chart-3: oklch(0.5 0.22 245);
|
||||
--chart-4: oklch(0.42 0.1 245);
|
||||
--chart-5: oklch(0.55 0.14 245);
|
||||
--sidebar: oklch(0.98 0 240);
|
||||
--sidebar-foreground: oklch(0.32 0 0);
|
||||
@@ -339,10 +339,10 @@
|
||||
--border: oklch(0.3 0.04 245);
|
||||
--input: oklch(0.3 0.04 245);
|
||||
--ring: oklch(0.5 0.16 245);
|
||||
--chart-1: oklch(0.50 0.16 245);
|
||||
--chart-2: oklch(0.60 0.20 245);
|
||||
--chart-3: oklch(0.70 0.22 245);
|
||||
--chart-4: oklch(0.58 0.10 245);
|
||||
--chart-1: oklch(0.5 0.16 245);
|
||||
--chart-2: oklch(0.6 0.2 245);
|
||||
--chart-3: oklch(0.7 0.22 245);
|
||||
--chart-4: oklch(0.58 0.1 245);
|
||||
--chart-5: oklch(0.74 0.14 245);
|
||||
--sidebar: oklch(0.15 0.02 245);
|
||||
--sidebar-foreground: oklch(0.92 0 0);
|
||||
|
||||
Reference in New Issue
Block a user