feat(sidebar): add conversation management dialog with filtering and bulk actions
This commit is contained in:
500
src/components/conversations/conversation-manage-dialog.tsx
Normal file
500
src/components/conversations/conversation-manage-dialog.tsx
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import {
|
||||||
|
CheckSquare,
|
||||||
|
ChevronDown,
|
||||||
|
ListChecks,
|
||||||
|
Loader2,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
import { AgentIcon } from "@/components/agent-icon"
|
||||||
|
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
||||||
|
import { useTabContext } from "@/contexts/tab-context"
|
||||||
|
import {
|
||||||
|
deleteConversation,
|
||||||
|
listAllConversations,
|
||||||
|
updateConversationStatus,
|
||||||
|
} from "@/lib/api"
|
||||||
|
import type {
|
||||||
|
AgentType,
|
||||||
|
ConversationStatus,
|
||||||
|
DbConversationSummary,
|
||||||
|
} from "@/lib/types"
|
||||||
|
import {
|
||||||
|
AGENT_LABELS,
|
||||||
|
ALL_AGENT_TYPES,
|
||||||
|
STATUS_COLORS,
|
||||||
|
STATUS_ORDER,
|
||||||
|
} from "@/lib/types"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface ConversationManageDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
folderId: number
|
||||||
|
folderName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimestamp(value: string): number {
|
||||||
|
const ts = Date.parse(value)
|
||||||
|
return Number.isNaN(ts) ? 0 : ts
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelative(iso: string): string {
|
||||||
|
const ts = parseTimestamp(iso)
|
||||||
|
if (!ts) return ""
|
||||||
|
const diff = Math.max(0, Date.now() - ts)
|
||||||
|
const m = Math.floor(diff / 60000)
|
||||||
|
if (m < 1) return "now"
|
||||||
|
if (m < 60) return `${m}m`
|
||||||
|
const h = Math.floor(m / 60)
|
||||||
|
if (h < 24) return `${h}h`
|
||||||
|
const d = Math.floor(h / 24)
|
||||||
|
if (d < 30) return `${d}d`
|
||||||
|
const mo = Math.floor(d / 30)
|
||||||
|
if (mo < 12) return `${mo}mo`
|
||||||
|
const y = Math.floor(mo / 12)
|
||||||
|
return `${y}y`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConversationManageDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
folderId,
|
||||||
|
folderName,
|
||||||
|
}: ConversationManageDialogProps) {
|
||||||
|
const t = useTranslations("Folder.sidebar.manageConversations")
|
||||||
|
const tCommon = useTranslations("Folder.common")
|
||||||
|
const tStatus = useTranslations("Folder.statusLabels")
|
||||||
|
|
||||||
|
const { refreshConversations } = useAppWorkspace()
|
||||||
|
const { closeConversationTab } = useTabContext()
|
||||||
|
|
||||||
|
const [search, setSearch] = useState("")
|
||||||
|
const [agentFilter, setAgentFilter] = useState<AgentType | "all">("all")
|
||||||
|
const [statusFilter, setStatusFilter] = useState<ConversationStatus | "all">(
|
||||||
|
"all"
|
||||||
|
)
|
||||||
|
const [rows, setRows] = useState<DbConversationSummary[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [selected, setSelected] = useState<Set<number>>(new Set())
|
||||||
|
const [pending, setPending] = useState(false)
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0)
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||||
|
|
||||||
|
// Reset state on open/close transitions
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setSearch("")
|
||||||
|
setAgentFilter("all")
|
||||||
|
setStatusFilter("all")
|
||||||
|
setSelected(new Set())
|
||||||
|
setConfirmDelete(false)
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
// Debounced data fetch
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await listAllConversations({
|
||||||
|
folder_ids: [folderId],
|
||||||
|
search: search.trim() || null,
|
||||||
|
agent_type: agentFilter === "all" ? null : agentFilter,
|
||||||
|
status: statusFilter === "all" ? null : statusFilter,
|
||||||
|
})
|
||||||
|
setRows(data)
|
||||||
|
setError(null)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : String(e))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [open, folderId, search, agentFilter, statusFilter, refreshKey])
|
||||||
|
|
||||||
|
const toggleOne = useCallback((id: number) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const allVisibleSelected = useMemo(
|
||||||
|
() => rows.length > 0 && rows.every((r) => selected.has(r.id)),
|
||||||
|
[rows, selected]
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleSelectAll = useCallback(() => {
|
||||||
|
if (allVisibleSelected) {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
for (const r of rows) next.delete(r.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
for (const r of rows) next.add(r.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [allVisibleSelected, rows])
|
||||||
|
|
||||||
|
const afterBulkOp = useCallback(() => {
|
||||||
|
setSelected(new Set())
|
||||||
|
setRefreshKey((k) => k + 1)
|
||||||
|
refreshConversations()
|
||||||
|
}, [refreshConversations])
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => [...selected], [selected])
|
||||||
|
const selectedCount = selected.size
|
||||||
|
|
||||||
|
const handleBulkDelete = useCallback(async () => {
|
||||||
|
if (selectedIds.length === 0) return
|
||||||
|
setPending(true)
|
||||||
|
try {
|
||||||
|
const affected = rows.filter((r) => selected.has(r.id))
|
||||||
|
await Promise.all(selectedIds.map((id) => deleteConversation(id)))
|
||||||
|
for (const conv of affected) {
|
||||||
|
closeConversationTab(conv.folder_id, conv.id, conv.agent_type)
|
||||||
|
}
|
||||||
|
toast.success(t("toastDeleted", { count: selectedIds.length }))
|
||||||
|
afterBulkOp()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(
|
||||||
|
t("toastOpFailed", {
|
||||||
|
message: e instanceof Error ? e.message : String(e),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setPending(false)
|
||||||
|
setConfirmDelete(false)
|
||||||
|
}
|
||||||
|
}, [selectedIds, rows, selected, closeConversationTab, t, afterBulkOp])
|
||||||
|
|
||||||
|
const handleBulkStatus = useCallback(
|
||||||
|
async (status: ConversationStatus) => {
|
||||||
|
if (selectedIds.length === 0) return
|
||||||
|
setPending(true)
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
selectedIds.map((id) => updateConversationStatus(id, status))
|
||||||
|
)
|
||||||
|
toast.success(t("toastStatusUpdated", { count: selectedIds.length }))
|
||||||
|
afterBulkOp()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(
|
||||||
|
t("toastOpFailed", {
|
||||||
|
message: e instanceof Error ? e.message : String(e),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setPending(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedIds, t, afterBulkOp]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("title", { name: folderName })}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Filter row */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder={t("searchPlaceholder")}
|
||||||
|
className="h-9 w-64"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={agentFilter}
|
||||||
|
onValueChange={(v) => setAgentFilter(v as AgentType | "all")}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("agentFilterAll")}</SelectItem>
|
||||||
|
{ALL_AGENT_TYPES.map((at) => (
|
||||||
|
<SelectItem key={at} value={at}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<AgentIcon agentType={at} className="h-3.5 w-3.5" />
|
||||||
|
{AGENT_LABELS[at]}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setStatusFilter(v as ConversationStatus | "all")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("statusFilterAll")}</SelectItem>
|
||||||
|
{STATUS_ORDER.map((s) => (
|
||||||
|
<SelectItem key={s} value={s}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-2 rounded-full",
|
||||||
|
STATUS_COLORS[s]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{tStatus(s)}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List container: select-all header + scrollable list */}
|
||||||
|
<div className="flex flex-col rounded-md border border-border/50 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50 bg-muted/20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleSelectAll}
|
||||||
|
disabled={rows.length === 0}
|
||||||
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span className="flex h-5 w-5 items-center justify-center">
|
||||||
|
{allVisibleSelected ? (
|
||||||
|
<CheckSquare className="h-4 w-4 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{allVisibleSelected ? t("deselectAll") : t("selectAllVisible")}
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t("matchedCount", { count: rows.length })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-[26rem]">
|
||||||
|
<div className="flex flex-col gap-0.5 p-1">
|
||||||
|
{loading ? (
|
||||||
|
Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-9 w-full rounded-md" />
|
||||||
|
))
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-destructive text-sm px-3 py-6 text-center">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm px-3 py-6 text-center">
|
||||||
|
{search.trim() ||
|
||||||
|
agentFilter !== "all" ||
|
||||||
|
statusFilter !== "all"
|
||||||
|
? t("noMatchingConversations")
|
||||||
|
: t("noConversations")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
rows.map((conv) => {
|
||||||
|
const checked = selected.has(conv.id)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={conv.id}
|
||||||
|
onClick={() => toggleOne(conv.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md px-2 py-1.5 cursor-pointer border border-transparent",
|
||||||
|
"hover:bg-accent/50",
|
||||||
|
checked && "bg-accent/40 border-accent/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
toggleOne(conv.id)
|
||||||
|
}}
|
||||||
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
|
||||||
|
aria-pressed={checked}
|
||||||
|
>
|
||||||
|
{checked ? (
|
||||||
|
<CheckSquare className="h-4 w-4 text-primary" />
|
||||||
|
) : (
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<AgentIcon
|
||||||
|
agentType={conv.agent_type}
|
||||||
|
className="h-4 w-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="flex-1 min-w-0 truncate text-sm">
|
||||||
|
{conv.title || t("untitledConversation")}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground tabular-nums w-14 text-right">
|
||||||
|
{t("messagesShort", { count: conv.message_count })}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground w-10 text-right">
|
||||||
|
{formatRelative(conv.updated_at)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 h-2 w-2 rounded-full",
|
||||||
|
STATUS_COLORS[conv.status as ConversationStatus] ??
|
||||||
|
"bg-gray-400"
|
||||||
|
)}
|
||||||
|
title={
|
||||||
|
STATUS_ORDER.includes(
|
||||||
|
conv.status as ConversationStatus
|
||||||
|
)
|
||||||
|
? tStatus(conv.status as ConversationStatus)
|
||||||
|
: conv.status
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer: bulk actions */}
|
||||||
|
<DialogFooter className="flex items-center justify-between gap-2 sm:justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t("selectedCount", { count: selectedCount })}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{/* Set status */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={selectedCount === 0 || pending}
|
||||||
|
>
|
||||||
|
<ListChecks className="h-3.5 w-3.5 mr-1" />
|
||||||
|
{t("setStatus")}
|
||||||
|
<ChevronDown className="h-3 w-3 ml-1 opacity-60" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{STATUS_ORDER.map((s) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={s}
|
||||||
|
onSelect={() => handleBulkStatus(s)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-2 rounded-full mr-2",
|
||||||
|
STATUS_COLORS[s]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{tStatus(s)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={selectedCount === 0 || pending}
|
||||||
|
onClick={() => setConfirmDelete(true)}
|
||||||
|
>
|
||||||
|
{pending ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||||
|
)}
|
||||||
|
{t("deleteSelected")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{tCommon("close")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete confirmation */}
|
||||||
|
<AlertDialog
|
||||||
|
open={confirmDelete}
|
||||||
|
onOpenChange={(o) => !o && setConfirmDelete(false)}
|
||||||
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("confirmDeleteTitle", { count: selectedCount })}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("confirmDeleteDescription")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleBulkDelete}>
|
||||||
|
{tCommon("confirm")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,7 +13,14 @@ import {
|
|||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Virtualizer, type VirtualizerHandle } from "virtua"
|
import { Virtualizer, type VirtualizerHandle } from "virtua"
|
||||||
import { ChevronRight, Download, Loader2, Plus, XCircle } from "lucide-react"
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Download,
|
||||||
|
ListChecks,
|
||||||
|
Loader2,
|
||||||
|
Plus,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react"
|
||||||
import { useActiveFolder } from "@/contexts/active-folder-context"
|
import { useActiveFolder } from "@/contexts/active-folder-context"
|
||||||
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
import { useAppWorkspace } from "@/contexts/app-workspace-context"
|
||||||
import { useTabContext } from "@/contexts/tab-context"
|
import { useTabContext } from "@/contexts/tab-context"
|
||||||
@@ -31,6 +38,7 @@ import {
|
|||||||
saveFolderExpanded,
|
saveFolderExpanded,
|
||||||
} from "@/lib/sidebar-view-mode-storage"
|
} from "@/lib/sidebar-view-mode-storage"
|
||||||
import { SidebarConversationCard } from "./sidebar-conversation-card"
|
import { SidebarConversationCard } from "./sidebar-conversation-card"
|
||||||
|
import { ConversationManageDialog } from "./conversation-manage-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
@@ -112,6 +120,7 @@ const FolderHeader = memo(function FolderHeader({
|
|||||||
onRemoveFromWorkspace,
|
onRemoveFromWorkspace,
|
||||||
onNewConversation,
|
onNewConversation,
|
||||||
onImport,
|
onImport,
|
||||||
|
onManageConversations,
|
||||||
t,
|
t,
|
||||||
}: {
|
}: {
|
||||||
folderId: number
|
folderId: number
|
||||||
@@ -123,6 +132,7 @@ const FolderHeader = memo(function FolderHeader({
|
|||||||
onRemoveFromWorkspace: (folderId: number) => void
|
onRemoveFromWorkspace: (folderId: number) => void
|
||||||
onNewConversation: (folderId: number) => void
|
onNewConversation: (folderId: number) => void
|
||||||
onImport: (folderId: number) => void
|
onImport: (folderId: number) => void
|
||||||
|
onManageConversations: (folderId: number) => void
|
||||||
t: ReturnType<typeof useTranslations>
|
t: ReturnType<typeof useTranslations>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -216,6 +226,11 @@ const FolderHeader = memo(function FolderHeader({
|
|||||||
{importing ? t("importing") : t("importLocalSessions")}
|
{importing ? t("importing") : t("importLocalSessions")}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator />
|
||||||
|
<ContextMenuItem onSelect={() => onManageConversations(folderId)}>
|
||||||
|
<ListChecks className="h-4 w-4" />
|
||||||
|
{t("folderHeaderMenu.manageConversations")}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onSelect={() => onRemoveFromWorkspace(folderId)}
|
onSelect={() => onRemoveFromWorkspace(folderId)}
|
||||||
@@ -301,6 +316,10 @@ export function SidebarConversationList({
|
|||||||
folderId: number
|
folderId: number
|
||||||
folderName: string
|
folderName: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const [manageState, setManageState] = useState<{
|
||||||
|
folderId: number
|
||||||
|
folderName: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Hydrate from localStorage after mount to keep SSR/CSR markup consistent.
|
// Hydrate from localStorage after mount to keep SSR/CSR markup consistent.
|
||||||
@@ -483,6 +502,14 @@ export function SidebarConversationList({
|
|||||||
[folderIndex]
|
[folderIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleManageConversations = useCallback(
|
||||||
|
(folderId: number) => {
|
||||||
|
const name = folderIndex.get(folderId)?.name ?? String(folderId)
|
||||||
|
setManageState({ folderId, folderName: name })
|
||||||
|
},
|
||||||
|
[folderIndex]
|
||||||
|
)
|
||||||
|
|
||||||
const handleRemoveFolderConfirm = useCallback(async () => {
|
const handleRemoveFolderConfirm = useCallback(async () => {
|
||||||
if (!removeConfirm) return
|
if (!removeConfirm) return
|
||||||
const { folderId, folderName } = removeConfirm
|
const { folderId, folderName } = removeConfirm
|
||||||
@@ -705,6 +732,7 @@ export function SidebarConversationList({
|
|||||||
onRemoveFromWorkspace={handleRemoveFolder}
|
onRemoveFromWorkspace={handleRemoveFolder}
|
||||||
onNewConversation={handleNewConversationForFolder}
|
onNewConversation={handleNewConversationForFolder}
|
||||||
onImport={handleImportForFolder}
|
onImport={handleImportForFolder}
|
||||||
|
onManageConversations={handleManageConversations}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -735,6 +763,7 @@ export function SidebarConversationList({
|
|||||||
onRemoveFromWorkspace={handleRemoveFolder}
|
onRemoveFromWorkspace={handleRemoveFolder}
|
||||||
onNewConversation={handleNewConversationForFolder}
|
onNewConversation={handleNewConversationForFolder}
|
||||||
onImport={handleImportForFolder}
|
onImport={handleImportForFolder}
|
||||||
|
onManageConversations={handleManageConversations}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -795,6 +824,15 @@ export function SidebarConversationList({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{manageState && (
|
||||||
|
<ConversationManageDialog
|
||||||
|
open
|
||||||
|
onOpenChange={(o) => !o && setManageState(null)}
|
||||||
|
folderId={manageState.folderId}
|
||||||
|
folderName={manageState.folderName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "إزالة المجلد من مساحة العمل؟",
|
"removeFolderConfirmTitle": "إزالة المجلد من مساحة العمل؟",
|
||||||
"removeFolderConfirmDescription": "إزالة \"{name}\" من مساحة العمل؟ سيتم إغلاق علامات التبويب والمحطات المرتبطة.",
|
"removeFolderConfirmDescription": "إزالة \"{name}\" من مساحة العمل؟ سيتم إغلاق علامات التبويب والمحطات المرتبطة.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "إدارة المحادثات…",
|
||||||
"removeFromWorkspace": "إزالة من مساحة العمل"
|
"removeFromWorkspace": "إزالة من مساحة العمل"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "إدارة المحادثات — {name}",
|
||||||
|
"searchPlaceholder": "البحث بالعنوان…",
|
||||||
|
"agentFilterAll": "جميع الوكلاء",
|
||||||
|
"statusFilterAll": "جميع الحالات",
|
||||||
|
"selectAllVisible": "تحديد الكل",
|
||||||
|
"deselectAll": "إلغاء التحديد",
|
||||||
|
"selectedCount": "{count} محدد",
|
||||||
|
"matchedCount": "{count} مطابق",
|
||||||
|
"messagesShort": "{count} رسالة",
|
||||||
|
"untitledConversation": "محادثة بدون عنوان",
|
||||||
|
"setStatus": "تعيين الحالة…",
|
||||||
|
"deleteSelected": "حذف",
|
||||||
|
"noConversations": "لا توجد محادثات في هذا المجلد.",
|
||||||
|
"noMatchingConversations": "لا توجد محادثات مطابقة للفلاتر.",
|
||||||
|
"confirmDeleteTitle": "حذف {count} محادثة؟",
|
||||||
|
"confirmDeleteDescription": "لا يمكن التراجع عن هذا الإجراء.",
|
||||||
|
"toastDeleted": "تم حذف {count} محادثة",
|
||||||
|
"toastStatusUpdated": "تم تحديث حالة {count} محادثة",
|
||||||
|
"toastOpFailed": "فشلت العملية: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "Ordner aus Arbeitsbereich entfernen?",
|
"removeFolderConfirmTitle": "Ordner aus Arbeitsbereich entfernen?",
|
||||||
"removeFolderConfirmDescription": "\"{name}\" aus dem Arbeitsbereich entfernen? Zugehörige Tabs und Terminals werden geschlossen.",
|
"removeFolderConfirmDescription": "\"{name}\" aus dem Arbeitsbereich entfernen? Zugehörige Tabs und Terminals werden geschlossen.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "Konversationen verwalten…",
|
||||||
"removeFromWorkspace": "Aus Arbeitsbereich entfernen"
|
"removeFromWorkspace": "Aus Arbeitsbereich entfernen"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "Konversationen verwalten — {name}",
|
||||||
|
"searchPlaceholder": "Nach Titel suchen…",
|
||||||
|
"agentFilterAll": "Alle Agenten",
|
||||||
|
"statusFilterAll": "Alle Status",
|
||||||
|
"selectAllVisible": "Alle auswählen",
|
||||||
|
"deselectAll": "Auswahl aufheben",
|
||||||
|
"selectedCount": "{count} ausgewählt",
|
||||||
|
"matchedCount": "{count} Treffer",
|
||||||
|
"messagesShort": "{count} Nachr.",
|
||||||
|
"untitledConversation": "Unbenannte Konversation",
|
||||||
|
"setStatus": "Status setzen…",
|
||||||
|
"deleteSelected": "Löschen",
|
||||||
|
"noConversations": "Keine Konversationen in diesem Ordner.",
|
||||||
|
"noMatchingConversations": "Keine Konversationen entsprechen den Filtern.",
|
||||||
|
"confirmDeleteTitle": "{count} Konversation(en) löschen?",
|
||||||
|
"confirmDeleteDescription": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"toastDeleted": "{count} Konversation(en) gelöscht",
|
||||||
|
"toastStatusUpdated": "Status von {count} Konversation(en) aktualisiert",
|
||||||
|
"toastOpFailed": "Aktion fehlgeschlagen: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "Remove folder from workspace?",
|
"removeFolderConfirmTitle": "Remove folder from workspace?",
|
||||||
"removeFolderConfirmDescription": "Remove \"{name}\" from the workspace? Its tabs and terminals will close.",
|
"removeFolderConfirmDescription": "Remove \"{name}\" from the workspace? Its tabs and terminals will close.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "Manage conversations…",
|
||||||
"removeFromWorkspace": "Remove from workspace"
|
"removeFromWorkspace": "Remove from workspace"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "Manage conversations — {name}",
|
||||||
|
"searchPlaceholder": "Search by title…",
|
||||||
|
"agentFilterAll": "All agents",
|
||||||
|
"statusFilterAll": "All statuses",
|
||||||
|
"selectAllVisible": "Select all",
|
||||||
|
"deselectAll": "Deselect all",
|
||||||
|
"selectedCount": "{count} selected",
|
||||||
|
"matchedCount": "{count} matched",
|
||||||
|
"messagesShort": "{count} msg",
|
||||||
|
"untitledConversation": "Untitled conversation",
|
||||||
|
"setStatus": "Set status…",
|
||||||
|
"deleteSelected": "Delete",
|
||||||
|
"noConversations": "No conversations in this folder.",
|
||||||
|
"noMatchingConversations": "No conversations match the filters.",
|
||||||
|
"confirmDeleteTitle": "Delete {count} conversation(s)?",
|
||||||
|
"confirmDeleteDescription": "This action cannot be undone.",
|
||||||
|
"toastDeleted": "Deleted {count} conversation(s)",
|
||||||
|
"toastStatusUpdated": "Updated status for {count} conversation(s)",
|
||||||
|
"toastOpFailed": "Operation failed: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "¿Eliminar carpeta del espacio de trabajo?",
|
"removeFolderConfirmTitle": "¿Eliminar carpeta del espacio de trabajo?",
|
||||||
"removeFolderConfirmDescription": "¿Eliminar \"{name}\" del espacio de trabajo? Sus pestañas y terminales se cerrarán.",
|
"removeFolderConfirmDescription": "¿Eliminar \"{name}\" del espacio de trabajo? Sus pestañas y terminales se cerrarán.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "Gestionar conversaciones…",
|
||||||
"removeFromWorkspace": "Quitar del espacio de trabajo"
|
"removeFromWorkspace": "Quitar del espacio de trabajo"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "Gestionar conversaciones — {name}",
|
||||||
|
"searchPlaceholder": "Buscar por título…",
|
||||||
|
"agentFilterAll": "Todos los agentes",
|
||||||
|
"statusFilterAll": "Todos los estados",
|
||||||
|
"selectAllVisible": "Seleccionar todo",
|
||||||
|
"deselectAll": "Deseleccionar todo",
|
||||||
|
"selectedCount": "{count} seleccionada(s)",
|
||||||
|
"matchedCount": "{count} coincidencia(s)",
|
||||||
|
"messagesShort": "{count} msg",
|
||||||
|
"untitledConversation": "Conversación sin título",
|
||||||
|
"setStatus": "Cambiar estado…",
|
||||||
|
"deleteSelected": "Eliminar",
|
||||||
|
"noConversations": "No hay conversaciones en esta carpeta.",
|
||||||
|
"noMatchingConversations": "Ninguna conversación coincide con los filtros.",
|
||||||
|
"confirmDeleteTitle": "¿Eliminar {count} conversación(es)?",
|
||||||
|
"confirmDeleteDescription": "Esta acción no se puede deshacer.",
|
||||||
|
"toastDeleted": "Se eliminaron {count} conversación(es)",
|
||||||
|
"toastStatusUpdated": "Estado actualizado para {count} conversación(es)",
|
||||||
|
"toastOpFailed": "Error en la operación: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "Retirer le dossier de l'espace de travail ?",
|
"removeFolderConfirmTitle": "Retirer le dossier de l'espace de travail ?",
|
||||||
"removeFolderConfirmDescription": "Retirer \"{name}\" de l'espace de travail ? Les onglets et terminaux associés seront fermés.",
|
"removeFolderConfirmDescription": "Retirer \"{name}\" de l'espace de travail ? Les onglets et terminaux associés seront fermés.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "Gérer les conversations…",
|
||||||
"removeFromWorkspace": "Retirer de l'espace de travail"
|
"removeFromWorkspace": "Retirer de l'espace de travail"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "Gérer les conversations — {name}",
|
||||||
|
"searchPlaceholder": "Rechercher par titre…",
|
||||||
|
"agentFilterAll": "Tous les agents",
|
||||||
|
"statusFilterAll": "Tous les statuts",
|
||||||
|
"selectAllVisible": "Tout sélectionner",
|
||||||
|
"deselectAll": "Tout désélectionner",
|
||||||
|
"selectedCount": "{count} sélectionnée(s)",
|
||||||
|
"matchedCount": "{count} correspondance(s)",
|
||||||
|
"messagesShort": "{count} msg",
|
||||||
|
"untitledConversation": "Conversation sans titre",
|
||||||
|
"setStatus": "Définir le statut…",
|
||||||
|
"deleteSelected": "Supprimer",
|
||||||
|
"noConversations": "Aucune conversation dans ce dossier.",
|
||||||
|
"noMatchingConversations": "Aucune conversation ne correspond aux filtres.",
|
||||||
|
"confirmDeleteTitle": "Supprimer {count} conversation(s) ?",
|
||||||
|
"confirmDeleteDescription": "Cette action est irréversible.",
|
||||||
|
"toastDeleted": "{count} conversation(s) supprimée(s)",
|
||||||
|
"toastStatusUpdated": "Statut mis à jour pour {count} conversation(s)",
|
||||||
|
"toastOpFailed": "Échec de l'opération : {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "このフォルダをワークスペースから削除しますか?",
|
"removeFolderConfirmTitle": "このフォルダをワークスペースから削除しますか?",
|
||||||
"removeFolderConfirmDescription": "\"{name}\" をワークスペースから削除しますか?関連するタブとターミナルが閉じられます。",
|
"removeFolderConfirmDescription": "\"{name}\" をワークスペースから削除しますか?関連するタブとターミナルが閉じられます。",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "会話の管理…",
|
||||||
"removeFromWorkspace": "ワークスペースから削除"
|
"removeFromWorkspace": "ワークスペースから削除"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "会話の管理 — {name}",
|
||||||
|
"searchPlaceholder": "タイトルで検索…",
|
||||||
|
"agentFilterAll": "すべてのエージェント",
|
||||||
|
"statusFilterAll": "すべてのステータス",
|
||||||
|
"selectAllVisible": "すべて選択",
|
||||||
|
"deselectAll": "選択を解除",
|
||||||
|
"selectedCount": "{count} 件選択中",
|
||||||
|
"matchedCount": "{count} 件該当",
|
||||||
|
"messagesShort": "{count} 件",
|
||||||
|
"untitledConversation": "無題の会話",
|
||||||
|
"setStatus": "ステータスを変更…",
|
||||||
|
"deleteSelected": "削除",
|
||||||
|
"noConversations": "このフォルダには会話がありません。",
|
||||||
|
"noMatchingConversations": "条件に一致する会話がありません。",
|
||||||
|
"confirmDeleteTitle": "{count} 件の会話を削除しますか?",
|
||||||
|
"confirmDeleteDescription": "この操作は元に戻せません。",
|
||||||
|
"toastDeleted": "{count} 件の会話を削除しました",
|
||||||
|
"toastStatusUpdated": "{count} 件の会話のステータスを更新しました",
|
||||||
|
"toastOpFailed": "操作に失敗しました: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "이 폴더를 워크스페이스에서 제거하시겠습니까?",
|
"removeFolderConfirmTitle": "이 폴더를 워크스페이스에서 제거하시겠습니까?",
|
||||||
"removeFolderConfirmDescription": "워크스페이스에서 \"{name}\"을(를) 제거하시겠습니까? 관련 탭과 터미널이 닫힙니다.",
|
"removeFolderConfirmDescription": "워크스페이스에서 \"{name}\"을(를) 제거하시겠습니까? 관련 탭과 터미널이 닫힙니다.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "대화 관리…",
|
||||||
"removeFromWorkspace": "워크스페이스에서 제거"
|
"removeFromWorkspace": "워크스페이스에서 제거"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "대화 관리 — {name}",
|
||||||
|
"searchPlaceholder": "제목으로 검색…",
|
||||||
|
"agentFilterAll": "모든 에이전트",
|
||||||
|
"statusFilterAll": "모든 상태",
|
||||||
|
"selectAllVisible": "전체 선택",
|
||||||
|
"deselectAll": "선택 해제",
|
||||||
|
"selectedCount": "{count}개 선택됨",
|
||||||
|
"matchedCount": "{count}개 일치",
|
||||||
|
"messagesShort": "{count}개",
|
||||||
|
"untitledConversation": "제목 없는 대화",
|
||||||
|
"setStatus": "상태 변경…",
|
||||||
|
"deleteSelected": "삭제",
|
||||||
|
"noConversations": "이 폴더에 대화가 없습니다.",
|
||||||
|
"noMatchingConversations": "필터에 일치하는 대화가 없습니다.",
|
||||||
|
"confirmDeleteTitle": "{count}개 대화를 삭제하시겠습니까?",
|
||||||
|
"confirmDeleteDescription": "이 작업은 되돌릴 수 없습니다.",
|
||||||
|
"toastDeleted": "{count}개 대화를 삭제했습니다",
|
||||||
|
"toastStatusUpdated": "{count}개 대화의 상태를 업데이트했습니다",
|
||||||
|
"toastOpFailed": "작업 실패: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "Remover pasta do espaço de trabalho?",
|
"removeFolderConfirmTitle": "Remover pasta do espaço de trabalho?",
|
||||||
"removeFolderConfirmDescription": "Remover \"{name}\" do espaço de trabalho? As abas e terminais relacionados serão fechados.",
|
"removeFolderConfirmDescription": "Remover \"{name}\" do espaço de trabalho? As abas e terminais relacionados serão fechados.",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "Gerenciar conversas…",
|
||||||
"removeFromWorkspace": "Remover do espaço de trabalho"
|
"removeFromWorkspace": "Remover do espaço de trabalho"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "Gerenciar conversas — {name}",
|
||||||
|
"searchPlaceholder": "Pesquisar por título…",
|
||||||
|
"agentFilterAll": "Todos os agentes",
|
||||||
|
"statusFilterAll": "Todos os status",
|
||||||
|
"selectAllVisible": "Selecionar tudo",
|
||||||
|
"deselectAll": "Desmarcar tudo",
|
||||||
|
"selectedCount": "{count} selecionada(s)",
|
||||||
|
"matchedCount": "{count} correspondência(s)",
|
||||||
|
"messagesShort": "{count} msg",
|
||||||
|
"untitledConversation": "Conversa sem título",
|
||||||
|
"setStatus": "Definir status…",
|
||||||
|
"deleteSelected": "Excluir",
|
||||||
|
"noConversations": "Sem conversas nesta pasta.",
|
||||||
|
"noMatchingConversations": "Nenhuma conversa corresponde aos filtros.",
|
||||||
|
"confirmDeleteTitle": "Excluir {count} conversa(s)?",
|
||||||
|
"confirmDeleteDescription": "Esta ação não pode ser desfeita.",
|
||||||
|
"toastDeleted": "{count} conversa(s) excluída(s)",
|
||||||
|
"toastStatusUpdated": "Status atualizado para {count} conversa(s)",
|
||||||
|
"toastOpFailed": "Falha na operação: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "从工作区移除该文件夹?",
|
"removeFolderConfirmTitle": "从工作区移除该文件夹?",
|
||||||
"removeFolderConfirmDescription": "从工作区移除 \"{name}\"?其相关 Tab 与终端将会关闭。",
|
"removeFolderConfirmDescription": "从工作区移除 \"{name}\"?其相关 Tab 与终端将会关闭。",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "会话管理…",
|
||||||
"removeFromWorkspace": "从工作区移除"
|
"removeFromWorkspace": "从工作区移除"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "会话管理 — {name}",
|
||||||
|
"searchPlaceholder": "按标题搜索…",
|
||||||
|
"agentFilterAll": "全部智能体",
|
||||||
|
"statusFilterAll": "全部状态",
|
||||||
|
"selectAllVisible": "全选",
|
||||||
|
"deselectAll": "取消全选",
|
||||||
|
"selectedCount": "已选 {count} 条",
|
||||||
|
"matchedCount": "匹配 {count} 条",
|
||||||
|
"messagesShort": "{count} 条",
|
||||||
|
"untitledConversation": "未命名会话",
|
||||||
|
"setStatus": "设为状态…",
|
||||||
|
"deleteSelected": "删除",
|
||||||
|
"noConversations": "此文件夹暂无会话。",
|
||||||
|
"noMatchingConversations": "没有匹配过滤条件的会话。",
|
||||||
|
"confirmDeleteTitle": "删除 {count} 个会话?",
|
||||||
|
"confirmDeleteDescription": "此操作不可撤销。",
|
||||||
|
"toastDeleted": "已删除 {count} 个会话",
|
||||||
|
"toastStatusUpdated": "已更新 {count} 个会话的状态",
|
||||||
|
"toastOpFailed": "操作失败:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
@@ -796,7 +796,29 @@
|
|||||||
"removeFolderConfirmTitle": "從工作區移除此資料夾?",
|
"removeFolderConfirmTitle": "從工作區移除此資料夾?",
|
||||||
"removeFolderConfirmDescription": "從工作區移除 \"{name}\"?相關分頁與終端機將會關閉。",
|
"removeFolderConfirmDescription": "從工作區移除 \"{name}\"?相關分頁與終端機將會關閉。",
|
||||||
"folderHeaderMenu": {
|
"folderHeaderMenu": {
|
||||||
|
"manageConversations": "會話管理…",
|
||||||
"removeFromWorkspace": "從工作區移除"
|
"removeFromWorkspace": "從工作區移除"
|
||||||
|
},
|
||||||
|
"manageConversations": {
|
||||||
|
"title": "會話管理 — {name}",
|
||||||
|
"searchPlaceholder": "依標題搜尋…",
|
||||||
|
"agentFilterAll": "全部智能體",
|
||||||
|
"statusFilterAll": "全部狀態",
|
||||||
|
"selectAllVisible": "全選",
|
||||||
|
"deselectAll": "取消全選",
|
||||||
|
"selectedCount": "已選 {count} 筆",
|
||||||
|
"matchedCount": "符合 {count} 筆",
|
||||||
|
"messagesShort": "{count} 則",
|
||||||
|
"untitledConversation": "未命名會話",
|
||||||
|
"setStatus": "設為狀態…",
|
||||||
|
"deleteSelected": "刪除",
|
||||||
|
"noConversations": "此資料夾暫無會話。",
|
||||||
|
"noMatchingConversations": "沒有符合過濾條件的會話。",
|
||||||
|
"confirmDeleteTitle": "刪除 {count} 個會話?",
|
||||||
|
"confirmDeleteDescription": "此操作無法復原。",
|
||||||
|
"toastDeleted": "已刪除 {count} 個會話",
|
||||||
|
"toastStatusUpdated": "已更新 {count} 個會話的狀態",
|
||||||
|
"toastOpFailed": "操作失敗:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
|
|||||||
Reference in New Issue
Block a user