Studio: bulk export and import in Settings Chat Data, MCP pill off switch (#6141)

* Studio: bulk export and import in Settings Chat Data, MCP pill off switch

- Settings -> Chat -> Data gains Export Recents and Projects (Recents or
  Recents + Projects, Raw JSONL / CSV / ShareGPT, combined or per chat)
  and Import chats, reusing the sidebar Recents menu actions
- Extract bulkExportConversationsByScope so the sidebar and Settings share
  one implementation; expose the export and import helpers via the chat
  feature index
- MCP composer pill icon now swaps to an X on hover like Search, Code and
  RAG; clicking it turns MCP off without opening the server menu
- en and zh-CN locale strings added (parity check passes)

* Reveal the pill X on hover for off-switch icons regardless of active look

The X was gated on data-active, so an MCP pill with no servers enabled
(or a RAG pill without a model) closed on icon click but never showed
the affordance. Off-switch pills only render while their feature is on,
so hover now always reveals the X.

* Replace the Recents hamburger menu with an Export all chats link to Settings

Bulk export and import now live in Settings -> Chat -> Data, so the
sidebar Recents header menu is gone. Each chat's Export submenu gains
Export all chats, which opens Settings on the Chat tab.
This commit is contained in:
Daniel Han 2026-06-10 05:11:15 -07:00 committed by GitHub
commit dfb49fd2cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 115 deletions

View file

@ -13,15 +13,32 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
EXPORT_FORMATS_LIST,
bulkExportConversationsByScope,
clearAllChats,
countAllChats,
downloadChatExport,
importConversationsFromFile,
useChatRuntimeStore,
} from "@/features/chat";
import { useT } from "@/i18n";
import { Delete02Icon, Download01Icon } from "@hugeicons/core-free-icons";
import {
Delete02Icon,
Download01Icon,
Upload01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { SettingsRow } from "../components/settings-row";
import { SettingsSection } from "../components/settings-section";
@ -61,6 +78,25 @@ export function ChatTab() {
}
};
const importInputRef = useRef<HTMLInputElement>(null);
const handleImport = async (file: File) => {
try {
const imported = await importConversationsFromFile(file, null);
if (imported === 0) {
toast.info(t("settings.chat.importNoConversations"));
} else {
toast.success(
imported === 1
? t("settings.chat.importedOneChat")
: t("settings.chat.importedChatCount", { count: imported }),
);
setCount(await countAllChats().catch(() => count));
}
} catch {
toast.error(t("settings.chat.importFailed"));
}
};
const handleClear = async () => {
setClearing(true);
try {
@ -172,6 +208,89 @@ export function ChatTab() {
</Button>
</SettingsRow>
<SettingsRow
label={t("settings.chat.exportConversations")}
description={t("settings.chat.exportConversationsDescription")}
>
<DropdownMenu>
<DropdownMenuTrigger asChild={true}>
<Button variant="outline" size="sm" disabled={count === 0}>
<HugeiconsIcon
icon={Download01Icon}
className="size-3.5 mr-1.5"
/>
{t("settings.chat.exportConversationsAction")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{(
[
{ scope: "recents", label: "exportScopeRecents" },
{ scope: "all", label: "exportScopeAll" },
] as const
).map(({ scope, label }) => (
<DropdownMenuSub key={scope}>
<DropdownMenuSubTrigger>
<HugeiconsIcon
icon={Download01Icon}
className="size-3.5 mr-1"
/>
{t(`settings.chat.${label}`)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
{EXPORT_FORMATS_LIST.map(({ fmt, label: fmtLabel }) => (
<DropdownMenuItem
key={`${scope}-m-${fmt}`}
onSelect={() =>
void bulkExportConversationsByScope(scope, fmt, true)
}
>
{fmtLabel} {t("settings.chat.exportCombinedSuffix")}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
{EXPORT_FORMATS_LIST.map(({ fmt, label: fmtLabel }) => (
<DropdownMenuItem
key={`${scope}-s-${fmt}`}
onSelect={() =>
void bulkExportConversationsByScope(scope, fmt, false)
}
>
{fmtLabel} {t("settings.chat.exportPerChatSuffix")}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
))}
</DropdownMenuContent>
</DropdownMenu>
</SettingsRow>
<SettingsRow
label={t("settings.chat.importChats")}
description={t("settings.chat.importChatsDescription")}
>
<Button
variant="outline"
size="sm"
onClick={() => importInputRef.current?.click()}
>
<HugeiconsIcon icon={Upload01Icon} className="size-3.5 mr-1.5" />
{t("settings.chat.importChatsAction")}
</Button>
<input
ref={importInputRef}
type="file"
accept=".jsonl,.ndjson,.csv"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
e.target.value = "";
if (file) void handleImport(file);
}}
/>
</SettingsRow>
<SettingsRow
destructive
label={t("settings.chat.clearAllChats")}