Files
tkmind_go/ui/desktop/src/toasts.tsx
T

245 lines
6.4 KiB
TypeScript
Raw Normal View History

2025-03-27 11:07:27 -04:00
import { toast, ToastOptions } from 'react-toastify';
import { Button } from './components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './components/ui/Tooltip';
import Copy from './components/icons/Copy';
2025-10-30 18:12:46 -04:00
import { startNewSession } from './sessions';
import { useNavigation } from './hooks/useNavigation';
import {
GroupedExtensionLoadingToast,
ExtensionLoadingStatus,
} from './components/GroupedExtensionLoadingToast';
import { getInitialWorkingDir } from './utils/workingDir';
2025-03-27 11:07:27 -04:00
export interface ToastServiceOptions {
silent?: boolean;
shouldThrow?: boolean;
}
2025-10-30 18:12:46 -04:00
class ToastService {
2025-03-27 11:07:27 -04:00
private silent: boolean = false;
private shouldThrow: boolean = false;
// Create a singleton instance
private static instance: ToastService;
public static getInstance(): ToastService {
if (!ToastService.instance) {
ToastService.instance = new ToastService();
}
return ToastService.instance;
}
configure(options: ToastServiceOptions = {}): void {
if (options.silent !== undefined) {
this.silent = options.silent;
}
2025-04-09 11:14:56 -04:00
2025-03-27 11:07:27 -04:00
if (options.shouldThrow !== undefined) {
this.shouldThrow = options.shouldThrow;
}
}
2025-10-30 18:12:46 -04:00
error(props: ToastErrorProps): void {
2025-03-27 11:07:27 -04:00
if (!this.silent) {
2025-10-30 18:12:46 -04:00
toastError(props);
2025-03-27 11:07:27 -04:00
}
if (this.shouldThrow) {
2025-10-30 18:12:46 -04:00
throw new Error(props.msg);
2025-03-27 11:07:27 -04:00
}
}
loading({ title, msg }: { title: string; msg: string }): string | number | undefined {
if (this.silent) {
return undefined;
}
const toastId = toastLoading({ title, msg });
return toastId;
}
success({ title, msg }: { title: string; msg: string }): void {
if (this.silent) {
return;
}
toastSuccess({ title, msg });
}
dismiss(toastId?: string | number): void {
if (toastId) toast.dismiss(toastId);
}
/**
* Create a grouped extension loading toast that can be updated as extensions load
*/
extensionLoading(
extensions: ExtensionLoadingStatus[],
totalCount: number,
isComplete: boolean = false
): string | number {
if (this.silent) {
return 'silent';
}
const toastId = 'extension-loading';
2026-03-09 16:03:24 -04:00
const hasErrors = extensions.some((ext) => ext.status === 'error');
const autoClose = isComplete && !hasErrors ? 5000 : false;
// Check if toast already exists
if (toast.isActive(toastId)) {
// Update existing toast
toast.update(toastId, {
render: (
<GroupedExtensionLoadingToast
extensions={extensions}
totalCount={totalCount}
isComplete={isComplete}
/>
),
2026-03-09 16:03:24 -04:00
autoClose,
closeButton: true,
closeOnClick: false,
});
} else {
// Create new toast
toast(
<GroupedExtensionLoadingToast
extensions={extensions}
totalCount={totalCount}
isComplete={isComplete}
/>,
{
...commonToastOptions,
toastId,
2026-03-09 16:03:24 -04:00
autoClose,
closeButton: true,
closeOnClick: false, // Prevent closing when clicking to expand/collapse
}
);
}
return toastId;
}
2025-03-27 11:07:27 -04:00
/**
* Handle errors with consistent logging and toast notifications
* Consolidates the functionality of the original handleError function
*/
handleError(title: string, message: string, options: ToastServiceOptions = {}): void {
this.configure(options);
this.error({
2025-10-30 18:12:46 -04:00
title: title,
2025-03-27 11:07:27 -04:00
msg: message,
traceback: message,
});
}
}
// Export a singleton instance for use throughout the app
export const toastService = ToastService.getInstance();
// Re-export ExtensionLoadingStatus for convenience
export type { ExtensionLoadingStatus };
2025-03-27 11:07:27 -04:00
const commonToastOptions: ToastOptions = {
position: 'top-right',
closeButton: true,
2025-03-27 11:07:27 -04:00
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
};
type ToastSuccessProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
2025-10-30 18:12:46 -04:00
2025-03-27 11:07:27 -04:00
export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProps) {
return toast.success(
<div>
{title ? <strong className="font-medium">{title}</strong> : null}
{title ? <div>{msg}</div> : null}
</div>,
{ ...commonToastOptions, autoClose: 3000, ...toastOptions }
);
}
type ToastErrorProps = {
2025-10-30 18:12:46 -04:00
title: string;
msg: string;
2025-03-27 11:07:27 -04:00
traceback?: string;
2025-10-30 18:12:46 -04:00
recoverHints?: string;
2025-03-27 11:07:27 -04:00
};
2025-10-30 18:12:46 -04:00
function ToastErrorContent({
title,
msg,
traceback,
recoverHints,
}: Omit<ToastErrorProps, 'setView'>) {
const setView = useNavigation();
const showRecovery = recoverHints && setView;
const hasBoth = traceback && showRecovery;
const handleCopyError = async () => {
if (traceback) {
try {
await navigator.clipboard.writeText(traceback);
} catch (error) {
console.error('Failed to copy error:', error);
}
}
};
2025-10-30 18:12:46 -04:00
return (
<div className="flex gap-4 pr-8">
2025-03-27 11:07:27 -04:00
<div className="flex-grow">
2025-10-30 18:12:46 -04:00
{title && <strong className="font-medium">{title}</strong>}
{msg && <div>{msg}</div>}
2025-03-27 11:07:27 -04:00
</div>
2025-10-30 18:12:46 -04:00
<div className="flex-none flex items-center gap-2">
{showRecovery && (
2026-01-21 07:13:55 -10:00
<Button onClick={() => startNewSession(recoverHints, setView, getInitialWorkingDir())}>
Ask goose
</Button>
)}
{hasBoth && (
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={handleCopyError} shape="round" aria-label="Copy error">
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="z-[10000]">
Copy error
</TooltipContent>
</Tooltip>
)}
{traceback && !hasBoth && <Button onClick={handleCopyError}>Copy error</Button>}
2025-03-27 11:07:27 -04:00
</div>
2025-10-30 18:12:46 -04:00
</div>
);
}
export function toastError({ title, msg, traceback, recoverHints }: ToastErrorProps) {
return toast.error(
<ToastErrorContent title={title} msg={msg} traceback={traceback} recoverHints={recoverHints} />,
{ ...commonToastOptions, autoClose: traceback ? false : 5000 }
2025-03-27 11:07:27 -04:00
);
}
type ToastLoadingProps = {
title?: string;
msg?: string;
toastOptions?: ToastOptions;
};
export function toastLoading({ title, msg, toastOptions }: ToastLoadingProps) {
return toast.loading(
<div>
{title ? <strong className="font-medium">{title}</strong> : null}
{title ? <div>{msg}</div> : null}
</div>,
{ ...commonToastOptions, autoClose: false, ...toastOptions }
);
}