Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
/**
|
|
* useConversationExport — Encapsulates conversation export logic for the AI chat panel.
|
|
*
|
|
* Handles:
|
|
* - Export in markdown, JSON, and plain text formats
|
|
* - Object URL lifecycle management (creation, revocation, cleanup on unmount)
|
|
*/
|
|
|
|
import React, { useCallback, useEffect, useRef } from 'react';
|
|
import type { AISession } from '../../../infrastructure/ai/types';
|
|
import { exportAsMarkdown, exportAsJSON, exportAsPlainText, getExportFilename } from '../../../infrastructure/ai/conversationExport';
|
|
|
|
// -------------------------------------------------------------------
|
|
// Hook return type
|
|
// -------------------------------------------------------------------
|
|
|
|
export interface UseConversationExportReturn {
|
|
/** Trigger a download of the active session in the given format. */
|
|
handleExport: (format: 'md' | 'json' | 'txt') => void;
|
|
/** Ref to active object URLs for cleanup on unmount (exposed for the parent cleanup effect). */
|
|
activeObjectUrlsRef: React.MutableRefObject<Set<string>>;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Hook implementation
|
|
// -------------------------------------------------------------------
|
|
|
|
export function useConversationExport(
|
|
activeSession: AISession | null,
|
|
): UseConversationExportReturn {
|
|
// Ref to track active object URLs for cleanup on unmount (Issue #19)
|
|
const activeObjectUrlsRef = useRef<Set<string>>(new Set());
|
|
|
|
// Clean up object URLs on unmount
|
|
useEffect(() => {
|
|
const urls = activeObjectUrlsRef.current;
|
|
return () => {
|
|
urls.forEach(url => URL.revokeObjectURL(url));
|
|
urls.clear();
|
|
};
|
|
}, []);
|
|
|
|
const handleExport = useCallback((format: 'md' | 'json' | 'txt') => {
|
|
if (!activeSession) return;
|
|
let content: string;
|
|
switch (format) {
|
|
case 'md': content = exportAsMarkdown(activeSession); break;
|
|
case 'json': content = exportAsJSON(activeSession); break;
|
|
case 'txt': content = exportAsPlainText(activeSession); break;
|
|
}
|
|
const filename = getExportFilename(activeSession, format);
|
|
// Create a download blob
|
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
// Track URL for cleanup on unmount (Issue #19)
|
|
activeObjectUrlsRef.current.add(url);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
// Revoke after a generous delay to ensure download completes, then remove from tracking set
|
|
const revokeTimeout = setTimeout(() => {
|
|
URL.revokeObjectURL(url);
|
|
activeObjectUrlsRef.current.delete(url);
|
|
}, 60_000); // 60 seconds to be safe for large files
|
|
// If component unmounts before timeout, cleanup effect will revoke it
|
|
void revokeTimeout; // suppress unused warning
|
|
}, [activeSession]);
|
|
|
|
return {
|
|
handleExport,
|
|
activeObjectUrlsRef,
|
|
};
|
|
}
|