mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
refactor(accounts): consolidate continuity guidance layout
- replace separate readiness and learning cards with one continuity overview surface - simplify row labels and dialog controls around continuity mode changes - keep localized continuity guidance aligned with the new component structure Refs #904
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -105,31 +106,39 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
const mode = row.original.context_mode || 'isolated';
|
||||
if (mode === 'shared') {
|
||||
const group = row.original.context_group || 'default';
|
||||
const mainLabel =
|
||||
row.original.continuity_mode === 'deeper'
|
||||
? t('accountsTable.sharedGroupDeeper', { group })
|
||||
: row.original.continuity_inferred
|
||||
? t('accountsTable.sharedGroupLegacy', { group })
|
||||
: t('accountsTable.sharedGroupStandard', { group });
|
||||
const peerLabel =
|
||||
row.original.sameGroupPeerCount > 0
|
||||
? t('accountsTable.sameGroupPeerCount', { count: row.original.sameGroupPeerCount })
|
||||
: t('accountsTable.noSameGroupPeer');
|
||||
const isDeeper = row.original.continuity_mode === 'deeper';
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-muted-foreground">{mainLabel}</p>
|
||||
<p className="text-xs text-muted-foreground/80">{peerLabel}</p>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`font-mono text-[10px] uppercase px-1.5 py-0 border ${isDeeper ? 'text-indigo-700 border-indigo-300/60 bg-indigo-50/50 dark:text-indigo-300 dark:border-indigo-900/40 dark:bg-indigo-900/20' : 'text-emerald-700 border-emerald-300/60 bg-emerald-50/50 dark:text-emerald-300 dark:border-emerald-900/40 dark:bg-emerald-900/20'}`}
|
||||
>
|
||||
{isDeeper ? 'Deeper' : 'Shared'}
|
||||
</Badge>
|
||||
<span className="text-xs font-semibold text-foreground/80">{group}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{row.original.sameGroupPeerCount > 0
|
||||
? t('accountsTable.sameGroupPeerCount', {
|
||||
count: row.original.sameGroupPeerCount,
|
||||
})
|
||||
: t('accountsTable.noSameGroupPeer')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.original.context_inferred) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
{t('accountsTable.isolatedLegacy')}
|
||||
</p>
|
||||
<p className="text-xs text-amber-700/80 dark:text-amber-400/80">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-amber-700 border-amber-300/60 bg-amber-50/50 dark:text-amber-400 dark:border-amber-900/40 dark:bg-amber-900/20 font-mono text-[10px] uppercase px-1.5 py-0"
|
||||
>
|
||||
Legacy
|
||||
</Badge>
|
||||
<p className="text-[10px] text-amber-700/80 dark:text-amber-400/80 whitespace-nowrap">
|
||||
{t('accountsTable.legacyReview')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -137,9 +146,13 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-muted-foreground">{t('accountsTable.isolated')}</p>
|
||||
<p className="text-xs text-muted-foreground/80">{t('accountsTable.noHandoff')}</p>
|
||||
<div className="flex flex-col items-start gap-1.5">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="font-mono text-[10px] uppercase px-1.5 py-0 text-muted-foreground bg-muted/60 border-transparent shadow-none"
|
||||
>
|
||||
Isolated
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Link2,
|
||||
ShieldAlert,
|
||||
Unlink,
|
||||
Waves,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
|
||||
interface ContinuityOverviewProps {
|
||||
totalAccounts: number;
|
||||
isolatedCount: number;
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
sharedAloneCount: number;
|
||||
sharedPeerAccountCount: number; // The accounts in a viable group
|
||||
deeperReadyAccountCount: number;
|
||||
sharedGroups: string[];
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
legacyTargetCount: number;
|
||||
cliproxyCount: number;
|
||||
}
|
||||
|
||||
type ReadinessState =
|
||||
| 'single'
|
||||
| 'isolated'
|
||||
| 'shared-alone'
|
||||
| 'shared-standard'
|
||||
| 'partial'
|
||||
| 'ready';
|
||||
|
||||
const InfoTooltip = ({ titleKey, descKey }: { titleKey: string; descKey: string }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 rounded-full hover:bg-muted text-muted-foreground/70 transition-colors"
|
||||
>
|
||||
<Info className="h-3 w-3" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-72 p-4 rounded-xl shadow-lg border-border/50 text-sm"
|
||||
side="top"
|
||||
align="center"
|
||||
>
|
||||
<p className="font-semibold tracking-tight">{t(titleKey)}</p>
|
||||
<p className="mt-1.5 text-muted-foreground leading-relaxed">{t(descKey)}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export function ContinuityOverview({
|
||||
totalAccounts,
|
||||
isolatedCount,
|
||||
sharedStandardCount,
|
||||
deeperSharedCount,
|
||||
sharedAloneCount,
|
||||
sharedPeerAccountCount,
|
||||
deeperReadyAccountCount,
|
||||
sharedPeerGroups,
|
||||
deeperReadyGroups,
|
||||
legacyTargetCount,
|
||||
cliproxyCount,
|
||||
}: ContinuityOverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const readiness: ReadinessState =
|
||||
totalAccounts < 2
|
||||
? 'single'
|
||||
: sharedPeerGroups.length === 0
|
||||
? isolatedCount === totalAccounts
|
||||
? 'isolated'
|
||||
: 'shared-alone'
|
||||
: deeperReadyGroups.length === 0
|
||||
? 'shared-standard'
|
||||
: isolatedCount > 0 ||
|
||||
sharedAloneCount > 0 ||
|
||||
deeperReadyAccountCount < sharedPeerAccountCount ||
|
||||
deeperReadyGroups.length < sharedPeerGroups.length
|
||||
? 'partial'
|
||||
: 'ready';
|
||||
|
||||
const highlightGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || 'default';
|
||||
const hasMixedState =
|
||||
deeperReadyGroups.length > 0 &&
|
||||
(isolatedCount > 0 ||
|
||||
sharedStandardCount > 0 ||
|
||||
sharedPeerGroups.length > deeperReadyGroups.length);
|
||||
|
||||
const iconMap = {
|
||||
ready: <CheckCircle2 className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />,
|
||||
'shared-standard': <Link2 className="h-6 w-6 text-blue-600 dark:text-blue-400" />,
|
||||
single: <ShieldAlert className="h-6 w-6 text-stone-400" />,
|
||||
isolated: <Unlink className="h-6 w-6 text-amber-600 dark:text-amber-400" />,
|
||||
'shared-alone': <AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />,
|
||||
partial: <AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />,
|
||||
};
|
||||
|
||||
const currentIcon = iconMap[readiness];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Primary Status Bento Box */}
|
||||
<Card className="flex flex-col justify-between overflow-hidden relative group p-0 border-border bg-card shadow-sm hover:shadow-md transition-shadow duration-300">
|
||||
<CardContent className="p-6 flex flex-col h-full bg-gradient-to-br from-card to-muted/20 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className="rounded-2xl bg-background p-3 shadow-sm ring-1 ring-border/50 self-start shrink-0">
|
||||
{currentIcon}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-lg font-semibold tracking-tight">
|
||||
{t(`continuityReadiness.messages.${readiness}.title`, {
|
||||
group: highlightGroup,
|
||||
})}
|
||||
</h3>
|
||||
<Badge
|
||||
variant={readiness === 'ready' ? 'default' : 'secondary'}
|
||||
className="rounded-full px-2.5 py-0.5 font-medium shadow-sm"
|
||||
>
|
||||
{t(`continuityReadiness.state.${readiness}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground max-w-lg leading-relaxed">
|
||||
{t(`continuityReadiness.messages.${readiness}.description`, {
|
||||
group: highlightGroup,
|
||||
count: sharedAloneCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-4 flex flex-wrap items-center gap-2">
|
||||
{cliproxyCount > 0 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-blue-700 bg-blue-50/50 border-blue-200/60 dark:border-blue-900/40 dark:bg-blue-900/20 dark:text-blue-300"
|
||||
>
|
||||
{t('historySyncLearningMap.cliproxyManaged', { count: cliproxyCount })}
|
||||
</Badge>
|
||||
)}
|
||||
{legacyTargetCount > 0 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-amber-700 bg-amber-50/50 border-amber-200/60 dark:border-amber-900/40 dark:bg-amber-900/20 dark:text-amber-300"
|
||||
>
|
||||
{t('historySyncLearningMap.legacyConfirmation', { count: legacyTargetCount })}
|
||||
</Badge>
|
||||
)}
|
||||
{sharedPeerGroups.length > 0 && deeperReadyGroups.length === 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||
>
|
||||
Recommend: {highlightGroup}
|
||||
</Badge>
|
||||
)}
|
||||
{hasMixedState && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||
>
|
||||
Partial sync ({highlightGroup})
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Horizontal Progression Chain */}
|
||||
<div className="flex flex-col md:flex-row items-center gap-3">
|
||||
<div className="flex-1 w-full flex items-center justify-between p-3.5 rounded-xl border border-blue-300/40 bg-blue-50/50 dark:border-blue-900/30 dark:bg-blue-900/10 shadow-sm transition-colors hover:bg-blue-100/40 dark:hover:bg-blue-900/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Unlink className="h-4 w-4 text-blue-700/80 dark:text-blue-400/80" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-900 dark:text-blue-200">
|
||||
{t('historySyncLearningMap.isolated')}
|
||||
</span>
|
||||
<InfoTooltip titleKey="accountsPage.isolated" descKey="accountsPage.isolatedDesc" />
|
||||
</div>
|
||||
<span className="text-lg font-mono font-bold text-blue-900 dark:text-blue-200">
|
||||
{isolatedCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="hidden md:block h-4 w-4 text-muted-foreground/40 shrink-0" />
|
||||
|
||||
<div className="flex-1 w-full flex items-center justify-between p-3.5 rounded-xl border border-emerald-300/40 bg-emerald-50/50 dark:border-emerald-900/30 dark:bg-emerald-900/10 shadow-sm transition-colors hover:bg-emerald-100/40 dark:hover:bg-emerald-900/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 text-emerald-700/80 dark:text-emerald-400/80" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-emerald-900 dark:text-emerald-200">
|
||||
{t('historySyncLearningMap.shared')}
|
||||
</span>
|
||||
<InfoTooltip
|
||||
titleKey="accountsPage.sharedStandard"
|
||||
descKey="accountsPage.sharedStandardDesc"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-lg font-mono font-bold text-emerald-900 dark:text-emerald-200">
|
||||
{sharedStandardCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="hidden md:block h-4 w-4 text-muted-foreground/40 shrink-0" />
|
||||
|
||||
<div className="flex-1 w-full flex items-center justify-between p-3.5 rounded-xl border border-indigo-300/40 bg-indigo-50/50 dark:border-indigo-900/30 dark:bg-indigo-900/10 shadow-sm transition-colors hover:bg-indigo-100/40 dark:hover:bg-indigo-900/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Waves className="h-4 w-4 text-indigo-700/80 dark:text-indigo-400/80" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-indigo-900 dark:text-indigo-200">
|
||||
{t('historySyncLearningMap.deeper')}
|
||||
</span>
|
||||
<InfoTooltip
|
||||
titleKey="accountsPage.sharedDeeper"
|
||||
descKey="accountsPage.sharedDeeperDesc"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-lg font-mono font-bold text-indigo-900 dark:text-indigo-200">
|
||||
{deeperSharedCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { AlertTriangle, CheckCircle2, Link2, ShieldAlert } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface ContinuityReadinessCardProps {
|
||||
totalAccounts: number;
|
||||
isolatedCount: number;
|
||||
sharedAloneCount: number;
|
||||
sharedPeerAccountCount: number;
|
||||
deeperReadyAccountCount: number;
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
}
|
||||
|
||||
type ReadinessState =
|
||||
| 'single'
|
||||
| 'isolated'
|
||||
| 'shared-alone'
|
||||
| 'shared-standard'
|
||||
| 'partial'
|
||||
| 'ready';
|
||||
|
||||
export function ContinuityReadinessCard({
|
||||
totalAccounts,
|
||||
isolatedCount,
|
||||
sharedAloneCount,
|
||||
sharedPeerAccountCount,
|
||||
deeperReadyAccountCount,
|
||||
sharedPeerGroups,
|
||||
deeperReadyGroups,
|
||||
}: ContinuityReadinessCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const readiness: ReadinessState =
|
||||
totalAccounts < 2
|
||||
? 'single'
|
||||
: sharedPeerGroups.length === 0
|
||||
? isolatedCount === totalAccounts
|
||||
? 'isolated'
|
||||
: 'shared-alone'
|
||||
: deeperReadyGroups.length === 0
|
||||
? 'shared-standard'
|
||||
: isolatedCount > 0 ||
|
||||
sharedAloneCount > 0 ||
|
||||
deeperReadyAccountCount < sharedPeerAccountCount ||
|
||||
deeperReadyGroups.length < sharedPeerGroups.length
|
||||
? 'partial'
|
||||
: 'ready';
|
||||
|
||||
const highlightGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || 'default';
|
||||
const icon =
|
||||
readiness === 'ready' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
) : readiness === 'shared-standard' ? (
|
||||
<Link2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
) : readiness === 'single' ? (
|
||||
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
);
|
||||
const stepItems =
|
||||
readiness === 'single'
|
||||
? [
|
||||
t('continuityReadiness.singleSteps.addAccount'),
|
||||
t('continuityReadiness.singleSteps.sameGroupLater'),
|
||||
t('continuityReadiness.singleSteps.enableDeeperLater'),
|
||||
t('continuityReadiness.singleSteps.resumeOriginal'),
|
||||
]
|
||||
: [
|
||||
t('continuityReadiness.steps.syncBoth'),
|
||||
t('continuityReadiness.steps.sameGroup', { group: highlightGroup }),
|
||||
t('continuityReadiness.steps.enableDeeper'),
|
||||
t('continuityReadiness.steps.resumeOriginal'),
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{t('continuityReadiness.title')}</CardTitle>
|
||||
<CardDescription>{t('continuityReadiness.description')}</CardDescription>
|
||||
</div>
|
||||
<Badge variant={readiness === 'ready' ? 'default' : 'secondary'}>
|
||||
{t(`continuityReadiness.state.${readiness}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.isolated')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{isolatedCount}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.sharedPeers')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{sharedPeerAccountCount}</p>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{t('continuityReadiness.metrics.deeperReady')}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold">{deeperReadyAccountCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5">{icon}</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t(`continuityReadiness.messages.${readiness}.title`, { group: highlightGroup })}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`continuityReadiness.messages.${readiness}.description`, {
|
||||
group: highlightGroup,
|
||||
count: sharedAloneCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">{t('continuityReadiness.stepsTitle')}</p>
|
||||
<ol className="space-y-2 pl-5 text-sm text-muted-foreground">
|
||||
{stepItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -10,13 +10,8 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
import { Unlink, Link2, Waves, ShieldAlert, Info } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||
import { useUpdateAccountContext } from '@/hooks/use-accounts';
|
||||
@@ -117,20 +112,47 @@ export function EditAccountContextDialog({
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="context-mode">{t('editAccountContext.syncMode')}</Label>
|
||||
<Select value={mode} onValueChange={(value) => setMode(value as ContextMode)}>
|
||||
<SelectTrigger id="context-mode">
|
||||
<SelectValue placeholder={t('editAccountContext.selectContextMode')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="isolated">{t('editAccountContext.isolatedOption')}</SelectItem>
|
||||
<SelectItem value="shared">{t('editAccountContext.sharedOption')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{mode === 'shared'
|
||||
? t('editAccountContext.sharedModeHint')
|
||||
: t('editAccountContext.isolatedModeHint')}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">{t('editAccountContext.syncMode')}</Label>
|
||||
</div>
|
||||
<div
|
||||
className="flex p-0.5 bg-muted/60 hover:bg-muted/80 transition-colors rounded-xl border border-border/40 w-full"
|
||||
role="radiogroup"
|
||||
aria-label={t('editAccountContext.syncMode')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === 'isolated'}
|
||||
onClick={() => setMode('isolated')}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
mode === 'isolated'
|
||||
? 'bg-background text-blue-600 dark:text-blue-400 shadow-sm ring-1 ring-border/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Unlink className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{t('editAccountContext.isolatedOption')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === 'shared'}
|
||||
onClick={() => setMode('shared')}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
mode === 'shared'
|
||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Link2 className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{t('editAccountContext.sharedOption')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-1 px-1">
|
||||
{mode === 'isolated'
|
||||
? t('editAccountContext.isolatedModeHint')
|
||||
: t('editAccountContext.sharedModeHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -155,23 +177,49 @@ export function EditAccountContextDialog({
|
||||
|
||||
{mode === 'shared' && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="continuity-mode">{t('editAccountContext.continuityDepth')}</Label>
|
||||
<Select
|
||||
value={continuityMode}
|
||||
onValueChange={(value) => setContinuityMode(value as ContinuityMode)}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t('editAccountContext.continuityDepth')}
|
||||
</Label>
|
||||
</div>
|
||||
<div
|
||||
className="flex p-0.5 bg-muted/60 hover:bg-muted/80 transition-colors rounded-xl border border-border/40 w-full"
|
||||
role="radiogroup"
|
||||
aria-label={t('editAccountContext.continuityDepth')}
|
||||
>
|
||||
<SelectTrigger id="continuity-mode">
|
||||
<SelectValue placeholder={t('editAccountContext.selectContinuityDepth')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standard">{t('editAccountContext.standardOption')}</SelectItem>
|
||||
<SelectItem value="deeper">{t('editAccountContext.deeperOption')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{continuityMode === 'deeper'
|
||||
? t('editAccountContext.deeperHint')
|
||||
: t('editAccountContext.standardHint')}
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={continuityMode === 'standard'}
|
||||
onClick={() => setContinuityMode('standard')}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
continuityMode === 'standard'
|
||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Link2 className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{t('editAccountContext.standardOption')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={continuityMode === 'deeper'}
|
||||
onClick={() => setContinuityMode('deeper')}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
continuityMode === 'deeper'
|
||||
? 'bg-background text-indigo-600 dark:text-indigo-400 shadow-sm ring-1 ring-border/50'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Waves className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{t('editAccountContext.deeperOption')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pt-1 px-1">
|
||||
{continuityMode === 'standard'
|
||||
? t('editAccountContext.standardHint')
|
||||
: t('editAccountContext.deeperHint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -180,33 +228,51 @@ export function EditAccountContextDialog({
|
||||
{t('editAccountContext.credentialsIsolated')}
|
||||
</p>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 p-3 text-xs">
|
||||
<p className="font-medium text-foreground">
|
||||
{t('editAccountContext.implicationTitle')}
|
||||
</p>
|
||||
<div className="mt-2 space-y-1.5 text-muted-foreground">
|
||||
<div
|
||||
className={`rounded-[14px] border p-4 text-xs shadow-sm transition-colors ${mode === 'isolated' ? 'bg-blue-50/50 border-blue-200 dark:bg-blue-900/10 dark:border-blue-800/40' : 'bg-muted/40 border-border/60'}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{mode === 'isolated' ? (
|
||||
<p>{t('editAccountContext.isolatedImplication')}</p>
|
||||
<ShieldAlert className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<>
|
||||
<p>{t('editAccountContext.sameGroupRule', { group: normalizedGroup })}</p>
|
||||
<p>
|
||||
{sameGroupPeerCount > 0
|
||||
? t('editAccountContext.sameGroupPeerCount', { count: sameGroupPeerCount })
|
||||
: t('editAccountContext.noSameGroupPeer')}
|
||||
<Info className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div className="space-y-1.5 flex-1 text-muted-foreground leading-relaxed">
|
||||
{mode === 'isolated' ? (
|
||||
<p className="text-foreground font-medium selection:bg-blue-200">
|
||||
{t('editAccountContext.isolatedImplication')}
|
||||
</p>
|
||||
<p>
|
||||
{continuityMode === 'deeper'
|
||||
? sameGroupDeeperPeerCount > 0
|
||||
? t('editAccountContext.deeperReady', {
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<span className="text-foreground font-medium">
|
||||
{t('editAccountContext.sameGroupRule', { group: normalizedGroup })}
|
||||
</span>{' '}
|
||||
{sameGroupPeerCount > 0
|
||||
? t('editAccountContext.sameGroupPeerCount', { count: sameGroupPeerCount })
|
||||
: t('editAccountContext.noSameGroupPeer')}
|
||||
</p>
|
||||
{continuityMode === 'deeper' && (
|
||||
<p>
|
||||
{sameGroupDeeperPeerCount > 0 ? (
|
||||
t('editAccountContext.deeperReady', {
|
||||
count: sameGroupDeeperPeerCount,
|
||||
})
|
||||
: t('editAccountContext.deeperNeedsPeers')
|
||||
: t('editAccountContext.standardWarning')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p>{t('editAccountContext.resumeOriginalWarning')}</p>
|
||||
) : (
|
||||
<span className="text-amber-600 dark:text-amber-500">
|
||||
{t('editAccountContext.deeperNeedsPeers')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
className={`pt-1.5 text-[11px] ${mode === 'isolated' ? 'text-blue-700/70 dark:text-blue-300/60' : 'text-muted-foreground/70'}`}
|
||||
>
|
||||
{t('editAccountContext.resumeOriginalWarning')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowRightLeft,
|
||||
ChevronDown,
|
||||
Layers3,
|
||||
Link2,
|
||||
Unlink,
|
||||
Waves,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface HistorySyncLearningMapProps {
|
||||
isolatedCount: number;
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
sharedGroups: string[];
|
||||
sharedPeerGroups: string[];
|
||||
deeperReadyGroups: string[];
|
||||
legacyTargetCount: number;
|
||||
cliproxyCount: number;
|
||||
}
|
||||
|
||||
type StageTone = 'isolated' | 'shared' | 'deeper';
|
||||
|
||||
function StageTile({
|
||||
title,
|
||||
count,
|
||||
icon: Icon,
|
||||
tone,
|
||||
}: {
|
||||
title: string;
|
||||
count: number;
|
||||
icon: LucideIcon;
|
||||
tone: StageTone;
|
||||
}) {
|
||||
const toneClasses: Record<StageTone, { border: string; icon: string; count: string }> = {
|
||||
isolated: {
|
||||
border: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10',
|
||||
icon: 'text-blue-700 dark:text-blue-400',
|
||||
count: 'text-blue-700 dark:text-blue-400',
|
||||
},
|
||||
shared: {
|
||||
border:
|
||||
'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10',
|
||||
icon: 'text-emerald-700 dark:text-emerald-400',
|
||||
count: 'text-emerald-700 dark:text-emerald-400',
|
||||
},
|
||||
deeper: {
|
||||
border:
|
||||
'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10',
|
||||
icon: 'text-indigo-700 dark:text-indigo-400',
|
||||
count: 'text-indigo-700 dark:text-indigo-400',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('rounded-md border p-2.5', toneClasses[tone].border)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold">{title}</p>
|
||||
<Icon className={cn('h-3.5 w-3.5', toneClasses[tone].icon)} />
|
||||
</div>
|
||||
<p className={cn('mt-1 text-lg font-mono font-semibold', toneClasses[tone].count)}>{count}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HistorySyncLearningMap({
|
||||
isolatedCount,
|
||||
sharedStandardCount,
|
||||
deeperSharedCount,
|
||||
sharedGroups,
|
||||
sharedPeerGroups,
|
||||
deeperReadyGroups,
|
||||
legacyTargetCount,
|
||||
cliproxyCount,
|
||||
}: HistorySyncLearningMapProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const groupsToShow = sharedGroups.length > 0 ? sharedGroups : ['default'];
|
||||
const highlightGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || groupsToShow[0];
|
||||
const hasMixedState =
|
||||
deeperReadyGroups.length > 0 &&
|
||||
(isolatedCount > 0 ||
|
||||
sharedStandardCount > 0 ||
|
||||
sharedPeerGroups.length > deeperReadyGroups.length);
|
||||
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle className="text-base">{t('historySyncLearningMap.title')}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{t('historySyncLearningMap.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="outline">{t('historySyncLearningMap.learningMap')}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{cliproxyCount > 0 && (
|
||||
<div className="rounded-md border border-blue-300/60 bg-blue-50/40 px-3 py-2 text-xs text-blue-800 dark:border-blue-900/40 dark:bg-blue-900/10 dark:text-blue-300">
|
||||
{t('historySyncLearningMap.cliproxyManaged', { count: cliproxyCount })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-[1fr_auto_1fr_auto_1fr] sm:items-center">
|
||||
<StageTile
|
||||
title={t('historySyncLearningMap.isolated')}
|
||||
count={isolatedCount}
|
||||
icon={Unlink}
|
||||
tone="isolated"
|
||||
/>
|
||||
<div className="hidden sm:flex justify-center text-muted-foreground">
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
<StageTile
|
||||
title={t('historySyncLearningMap.shared')}
|
||||
count={sharedStandardCount}
|
||||
icon={Link2}
|
||||
tone="shared"
|
||||
/>
|
||||
<div className="hidden sm:flex justify-center text-muted-foreground">
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
<StageTile
|
||||
title={t('historySyncLearningMap.deeper')}
|
||||
count={deeperSharedCount}
|
||||
icon={Waves}
|
||||
tone="deeper"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
{sharedPeerGroups.length === 0
|
||||
? t('historySyncLearningMap.sameGroupRule')
|
||||
: deeperReadyGroups.length === 0
|
||||
? t('historySyncLearningMap.deeperRecommendation', { group: highlightGroup })
|
||||
: hasMixedState
|
||||
? t('historySyncLearningMap.partialGroup', { group: highlightGroup })
|
||||
: t('historySyncLearningMap.readyGroup', { group: highlightGroup })}
|
||||
</div>
|
||||
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-full justify-between rounded-md px-2 text-xs text-muted-foreground hover:bg-muted/40 hover:text-foreground"
|
||||
>
|
||||
<span>{t('historySyncLearningMap.showDetails')}</span>
|
||||
<ChevronDown className={cn('h-4 w-4 transition-transform', open && 'rotate-180')} />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2">
|
||||
<div className="grid gap-2 lg:grid-cols-2">
|
||||
<div className="rounded-md border bg-muted/20 p-2.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowRightLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<p className="font-semibold">{t('historySyncLearningMap.modeSwitch')}</p>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{t('historySyncLearningMap.modeSwitchDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/20 p-2.5 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers3 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<p className="font-semibold">{t('historySyncLearningMap.historySyncGroup')}</p>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{t('historySyncLearningMap.historySyncGroupDesc')}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{groupsToShow.map((group) => (
|
||||
<Badge key={group} variant="outline" className="font-mono text-[10px]">
|
||||
{group}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{legacyTargetCount > 0 && (
|
||||
<div className="mt-2 rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-xs text-amber-800 dark:text-amber-300">
|
||||
{t('historySyncLearningMap.legacyConfirmation', { count: legacyTargetCount })}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
+7
-10
@@ -401,12 +401,10 @@ const resources = {
|
||||
'Configure how "{{name}}" shares history and continuity with other ccs auth accounts.',
|
||||
syncMode: 'Sync Mode',
|
||||
selectContextMode: 'Select context mode',
|
||||
isolatedOption: 'isolated (no sync)',
|
||||
sharedOption: 'shared (sync enabled)',
|
||||
sharedModeHint:
|
||||
'Shared mode reuses workspace context for accounts in the same history sync group.',
|
||||
isolatedModeHint:
|
||||
'Isolated mode keeps this account fully separate from other ccs auth accounts.',
|
||||
isolatedOption: 'Isolated',
|
||||
sharedOption: 'Shared',
|
||||
sharedModeHint: 'Reuses workspace context across accounts in the same history group.',
|
||||
isolatedModeHint: 'Keeps this account fully separate from other ccs auth accounts.',
|
||||
historySyncGroup: 'History Sync Group',
|
||||
groupPlaceholder: 'default',
|
||||
groupHint:
|
||||
@@ -414,10 +412,9 @@ const resources = {
|
||||
invalidGroup: 'Enter a valid group name that starts with a letter.',
|
||||
continuityDepth: 'Continuity Depth',
|
||||
selectContinuityDepth: 'Select continuity depth',
|
||||
standardOption: 'standard (projects only)',
|
||||
deeperOption: 'deeper continuity (advanced)',
|
||||
deeperHint:
|
||||
'Advanced mode also syncs session-env, file-history, shell-snapshots, and todos.',
|
||||
standardOption: 'Standard',
|
||||
deeperOption: 'Deeper',
|
||||
deeperHint: 'Syncs comprehensive session-env, file-history, shell-snapshots, and todos.',
|
||||
standardHint: 'Standard mode syncs project workspace context only.',
|
||||
credentialsIsolated: 'Credentials and .anthropic remain isolated per account in all modes.',
|
||||
implicationTitle: 'What this means after save',
|
||||
|
||||
+11
-71
@@ -5,19 +5,16 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AlertTriangle, ArrowRight, ChevronDown, Plus, Users, Zap } from 'lucide-react';
|
||||
import { AlertTriangle, ArrowRight, Plus, Users, Zap } from 'lucide-react';
|
||||
import { AccountsTable } from '@/components/account/accounts-table';
|
||||
import { ContinuityReadinessCard } from '@/components/account/continuity-readiness-card';
|
||||
import { ContinuityOverview } from '@/components/account/continuity-overview';
|
||||
import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog';
|
||||
import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useAccounts, useConfirmLegacyAccountPolicies } from '@/hooks/use-accounts';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function AccountsPage() {
|
||||
@@ -26,7 +23,6 @@ export function AccountsPage() {
|
||||
const { data, isLoading } = useAccounts();
|
||||
const confirmLegacyMutation = useConfirmLegacyAccountPolicies();
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [guideOpen, setGuideOpen] = useState(false);
|
||||
|
||||
const authAccounts = data?.accounts || [];
|
||||
const cliproxyCount = data?.cliproxyCount || 0;
|
||||
@@ -146,50 +142,6 @@ export function AccountsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Collapsible open={guideOpen} onOpenChange={setGuideOpen}>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" className="h-auto w-full justify-between px-0 py-0">
|
||||
<div className="text-left">
|
||||
<CardTitle className="text-sm">
|
||||
{t('accountsPage.continuityGuide')}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{t('accountsPage.expandWhenNeeded')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn('h-4 w-4 transition-transform', guideOpen && 'rotate-180')}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</CardHeader>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-3 text-xs text-muted-foreground">
|
||||
<div className="rounded-md border p-2.5">
|
||||
<p className="font-semibold text-foreground">
|
||||
{t('accountsPage.sharedStandard')}
|
||||
</p>
|
||||
<p className="mt-1">{t('accountsPage.sharedStandardDesc')}</p>
|
||||
</div>
|
||||
<div className="rounded-md border p-2.5">
|
||||
<p className="font-semibold text-foreground">
|
||||
{t('accountsPage.sharedDeeper')}
|
||||
</p>
|
||||
<p className="mt-1">{t('accountsPage.sharedDeeperDesc')}</p>
|
||||
</div>
|
||||
<div className="rounded-md border p-2.5">
|
||||
<p className="font-semibold text-foreground">
|
||||
{t('accountsPage.isolated')}
|
||||
</p>
|
||||
<p className="mt-1">{t('accountsPage.isolatedDesc')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">{t('accountsPage.quickCommands')}</CardTitle>
|
||||
@@ -235,20 +187,14 @@ export function AccountsPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
|
||||
<ContinuityReadinessCard
|
||||
<ContinuityOverview
|
||||
totalAccounts={authAccounts.length}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
/>
|
||||
|
||||
<HistorySyncLearningMap
|
||||
isolatedCount={isolatedCount}
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedGroups={sharedGroups}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
@@ -316,10 +262,14 @@ export function AccountsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<HistorySyncLearningMap
|
||||
<ContinuityOverview
|
||||
totalAccounts={authAccounts.length}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedGroups={sharedGroups}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
@@ -327,16 +277,6 @@ export function AccountsPage() {
|
||||
cliproxyCount={cliproxyCount}
|
||||
/>
|
||||
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={authAccounts.length}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedAloneCount={sharedAloneCount}
|
||||
sharedPeerAccountCount={sharedPeerAccountCount}
|
||||
deeperReadyAccountCount={deeperReadyAccountCount}
|
||||
sharedPeerGroups={sharedPeerGroups}
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{t('accountsPage.accountMatrix')}</CardTitle>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { ContinuityReadinessCard } from '@/components/account/continuity-readiness-card';
|
||||
import { ContinuityOverview } from '@/components/account/continuity-overview';
|
||||
import { LanguageSwitcher } from '@/components/layout/language-switcher';
|
||||
import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map';
|
||||
import { TabNavigation } from '@/pages/settings/components/tab-navigation';
|
||||
import {
|
||||
getInitialLocale,
|
||||
@@ -138,97 +137,50 @@ describe('Dashboard i18n', () => {
|
||||
expect(screen.getByText('思考')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Japanese history sync guidance without fallback English copy', async () => {
|
||||
await i18n.changeLanguage('ja');
|
||||
|
||||
render(
|
||||
<HistorySyncLearningMap
|
||||
isolatedCount={1}
|
||||
sharedStandardCount={2}
|
||||
deeperSharedCount={3}
|
||||
sharedGroups={['default']}
|
||||
sharedPeerGroups={['default']}
|
||||
deeperReadyGroups={['default']}
|
||||
legacyTargetCount={2}
|
||||
cliproxyCount={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('履歴同期の仕組み')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'グループ「default」は良い状態ですが、他のアカウントやグループでは同グループ設定や拡張継続性がまだ不足しています。'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('1 件の CLIProxy Claude Pool アカウントは、CLIProxy ページで管理します。')
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: '詳細を表示: グループ、切り替え、レガシーポリシー' })
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('2 件のレガシーアカウントで明示的な確認がまだ必要です。')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Japanese continuity readiness card without fallback English copy', async () => {
|
||||
await i18n.changeLanguage('ja');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
totalAccounts={2}
|
||||
isolatedCount={2}
|
||||
sharedAloneCount={0}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('アカウント間再開チェック')).toBeInTheDocument();
|
||||
expect(screen.getByText('現在、アカウント間再開はオフです。')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses single-account-specific next steps in the readiness card', async () => {
|
||||
it('uses single-account-specific next steps in the continuity overview', async () => {
|
||||
await i18n.changeLanguage('en');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
<ContinuityOverview
|
||||
totalAccounts={1}
|
||||
isolatedCount={1}
|
||||
sharedStandardCount={0}
|
||||
deeperSharedCount={0}
|
||||
sharedAloneCount={0}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedGroups={[]}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
legacyTargetCount={0}
|
||||
cliproxyCount={0}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Create a second ccs auth account before planning any cross-account handoff.'
|
||||
'Cross-account handoff does not apply yet. Add another auth account before configuring shared continuity.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Open Sync on both accounts, not just the one you are switching into.')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pluralizes the shared-alone readiness copy', async () => {
|
||||
await i18n.changeLanguage('en');
|
||||
|
||||
render(
|
||||
<ContinuityReadinessCard
|
||||
<ContinuityOverview
|
||||
totalAccounts={3}
|
||||
isolatedCount={1}
|
||||
sharedStandardCount={2}
|
||||
deeperSharedCount={0}
|
||||
sharedAloneCount={2}
|
||||
sharedPeerAccountCount={0}
|
||||
deeperReadyAccountCount={0}
|
||||
sharedGroups={[]}
|
||||
sharedPeerGroups={[]}
|
||||
deeperReadyGroups={[]}
|
||||
legacyTargetCount={0}
|
||||
cliproxyCount={0}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -237,9 +189,9 @@ describe('Dashboard i18n', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(SUPPORTED_LOCALES.filter((locale) => locale !== 'en'))(
|
||||
it.each(SUPPORTED_LOCALES.filter((locale: string) => locale !== 'en'))(
|
||||
'keeps %s translation keys in parity with en and preserves placeholders',
|
||||
(locale) => {
|
||||
(locale: string) => {
|
||||
const resources = i18n.options.resources as
|
||||
| Record<string, { translation: Record<string, unknown> }>
|
||||
| undefined;
|
||||
|
||||
Reference in New Issue
Block a user