diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 59a498e5..1496e315 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -947,6 +947,8 @@ "Try a different search term.": "Try a different search term.", "Try again": "Try again", "Untitled chat": "Untitled chat", + "Export chat": "Export chat", + "You": "You", "What can I help you with?": "What can I help you with?", "Are you sure you want to revoke this {{credential}}": "Are you sure you want to revoke this {{credential}}", "Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.", diff --git a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx index 1e4b0ed8..ef3aabe6 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx +++ b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx @@ -10,6 +10,7 @@ import { Group, Loader, Tooltip } from "@mantine/core"; import { IconArrowsDiagonal, IconChevronDown, + IconFileExport, IconGripVertical, IconMinus, IconPlus, @@ -33,6 +34,7 @@ import { } from "@/features/ai-chat/queries/ai-chat-query.ts"; import ConversationList from "@/features/ai-chat/components/conversation-list.tsx"; import ChatThread from "@/features/ai-chat/components/chat-thread.tsx"; +import { exportChatAsMarkdown } from "@/features/ai-chat/utils/export-chat.ts"; import classes from "@/features/ai-chat/components/ai-chat-window.module.css"; // Default window geometry (from the GitmostAgent.jsx design). @@ -154,6 +156,26 @@ export default function AiChatWindow() { queryClient.invalidateQueries({ queryKey: AI_CHATS_RQ_KEY }); }, [activeChatId, queryClient]); + // The active chat object (for its title) and an export gate: only enable the + // export button when an existing chat with loaded persisted rows is active. + const activeChat = useMemo( + () => chats?.items?.find((c) => c.id === activeChatId) ?? null, + [chats, activeChatId], + ); + const canExport = !!activeChatId && !!messageRows && messageRows.length > 0; + + // Build a Markdown export from the already-loaded persisted rows (no network + // call) and trigger a browser download. The download dialog is the feedback. + const handleExport = useCallback(() => { + if (!activeChatId || !messageRows || messageRows.length === 0) return; + exportChatAsMarkdown({ + title: activeChat?.title ?? null, + chatId: activeChatId, + rows: messageRows, + t, + }); + }, [activeChatId, messageRows, activeChat, t]); + // When awaiting a new chat's id, adopt the most-recent chat (the list is // ordered newest-first) once it appears. useEffect(() => { @@ -308,6 +330,17 @@ export default function AiChatWindow() {
+ {canExport && ( + + )}