feat: 添加MinIO文件系统支持并优化外部连接器功能

- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等)
- 更新外部连接器配置结构,包括BASE_URL和认证令牌设置
- 改进connector provider支持更多类型(official, feishu_bot等)
- 实现Mistral模型推理模式支持reasoning_effort参数
- 增强外部连接器策略配置和运行时配置管理
- 添加connector bridge事件验证和安全保护机制
- 优化任务路由逻辑,区分simple_chat和new_task场景
- 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
2026-06-05 11:46:40 +08:00
parent 236ac19789
commit 2c5205b06e
120 changed files with 8321 additions and 1865 deletions

View File

@ -4,6 +4,7 @@ import React from 'react';
import { usePathname } from 'next/navigation';
import { getStatus, listSessions, wsManager } from '@/lib/api';
import { runtimeBridgeEnabledForPath } from '@/lib/channel-connector-state';
import { useChatStore } from '@/lib/store';
import type { ProcessWsEvent, SessionUpdatedEvent, WsEvent } from '@/types';
@ -47,7 +48,7 @@ export function AppRuntimeBridge() {
const ingestProcessEvent = useChatStore((state) => state.ingestProcessEvent);
const statusCheckCleanupRef = React.useRef<(() => void) | null>(null);
const statusCheckInFlightRef = React.useRef(false);
const chatRuntimeEnabled = pathname === '/' || pathname.startsWith('/tasks') || pathname.startsWith('/notifications');
const chatRuntimeEnabled = runtimeBridgeEnabledForPath(pathname);
const loadSessions = React.useCallback(async () => {
try {

View File

@ -3,7 +3,7 @@
import React from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, MessageSquare, Puzzle, Settings, Store, Wrench } from 'lucide-react';
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, Menu, MessageSquare, Puzzle, Settings, Store, Wrench, X } from 'lucide-react';
import { logout } from '@/lib/api';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
@ -69,6 +69,7 @@ const Header = () => {
const { locale } = useAppI18n();
const pathname = usePathname();
const router = useRouter();
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const user = useChatStore((s) => s.user);
const isAuthLoading = useChatStore((s) => s.isAuthLoading);
const setUser = useChatStore((s) => s.setUser);
@ -93,108 +94,175 @@ const Header = () => {
router.refresh();
};
React.useEffect(() => {
setMobileMenuOpen(false);
}, [pathname]);
React.useEffect(() => {
if (!mobileMenuOpen) return;
const previousOverflow = document.body.style.overflow;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setMobileMenuOpen(false);
}
};
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', handleKeyDown);
};
}, [mobileMenuOpen]);
const userInitial = (user?.username || user?.email || '?').trim().charAt(0).toUpperCase();
return (
<header className="fixed left-0 right-0 top-0 z-50 border-b border-[#E6E1DE] bg-[#F7F6F5]/95 backdrop-blur">
<div className="mx-auto max-w-[1720px] px-4 sm:px-6 lg:px-8">
<div className="grid h-16 grid-cols-[minmax(120px,1fr)_auto_minmax(120px,1fr)] items-center gap-4">
<Link href="/" className="flex shrink-0 items-center">
<span className="font-serif text-[28px] font-semibold leading-none text-[#0B0B0B]">
Beaver
</span>
</Link>
const renderNavLinks = (compact = false) =>
NAV_ITEMS.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: item.matchPrefixes?.some((prefix) => pathname.startsWith(prefix)) ?? pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
onClick={compact ? () => setMobileMenuOpen(false) : undefined}
className={`flex h-11 shrink-0 items-center gap-2 rounded-full text-sm font-medium transition-colors ${
compact ? 'justify-start rounded-lg border border-transparent bg-background px-4' : 'px-4'
} ${
isActive
? 'bg-primary text-primary-foreground'
: compact
? 'text-[#4F4642] hover:border-[#E6E1DE] hover:bg-muted hover:text-[#0B0B0B]'
: 'text-[#4F4642] hover:bg-[#F7F5F4] hover:text-[#0B0B0B]'
}`}
>
<Icon className="h-4 w-4" />
{navLabel(item.key)}
</Link>
);
});
<nav className="flex items-center gap-1 rounded-full border border-[#E6E1DE] bg-white px-1.5 py-1 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
{NAV_ITEMS.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: item.matchPrefixes?.some((prefix) => pathname.startsWith(prefix)) ?? pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`flex shrink-0 items-center gap-1.5 rounded-full px-4 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-primary text-primary-foreground'
: 'text-[#4F4642] hover:bg-[#F7F5F4] hover:text-[#0B0B0B]'
}`}
>
<Icon className="w-4 h-4" />
{navLabel(item.key)}
</Link>
);
})}
return (
<>
<header className="fixed left-0 right-0 top-0 z-50 border-b border-[#E6E1DE] bg-[#F7F6F5]/95 backdrop-blur">
<div className="mx-auto max-w-[1720px] px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<button
type="button"
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-[#E6E1DE] bg-white text-[#1D1715] transition-colors hover:bg-[#F7F5F4] 2xl:hidden"
aria-label={mobileMenuOpen ? pickAppText(locale, '关闭导航', 'Close navigation') : pickAppText(locale, '打开导航', 'Open navigation')}
aria-expanded={mobileMenuOpen}
aria-controls="app-primary-mobile-nav"
onClick={() => setMobileMenuOpen((open) => !open)}
>
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
<Link href="/" className="hidden h-11 shrink-0 items-center min-[360px]:flex">
<span className="font-serif text-[26px] font-semibold leading-none text-[#0B0B0B] sm:text-[28px]">
Beaver
</span>
</Link>
</div>
<nav className="hidden items-center gap-1 rounded-full border border-[#E6E1DE] bg-white px-1.5 py-1 shadow-[0_1px_2px_rgba(0,0,0,0.04)] 2xl:flex">
{renderNavLinks(false)}
</nav>
<div className="flex min-w-0 items-center justify-end gap-3">
<div className="hidden shrink-0 sm:block">
<ConnectionDot />
</div>
<div className="flex shrink-0 items-center gap-2">
<LanguageSwitcher />
{user ? (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="flex items-center gap-2 rounded-full border border-[#E6E1DE] bg-white px-2 py-1.5 text-sm font-medium text-[#1D1715] transition-colors hover:bg-[#F7F5F4]"
>
<Avatar className="h-8 w-8 border border-[#E6E1DE]">
<AvatarFallback className="bg-primary text-xs font-semibold text-primary-foreground">
{userInitial}
</AvatarFallback>
</Avatar>
<span className="hidden max-w-28 truncate sm:block">{user.username}</span>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 rounded-3xl border-border/70 p-0 shadow-2xl">
<div className="overflow-hidden rounded-3xl bg-[linear-gradient(180deg,#F7F5F4,#FFFFFF)]">
<div className="border-b border-border/60 px-6 py-5">
<p className="truncate text-center text-sm font-medium text-muted-foreground">
{user.email}
</p>
</div>
<div className="flex flex-col items-center gap-4 px-6 py-6 text-center">
<Avatar className="h-24 w-24 border-4 border-white shadow-sm">
<AvatarFallback className="bg-primary text-4xl font-semibold text-primary-foreground">
<div className="flex min-w-0 shrink-0 items-center justify-end gap-2 sm:gap-3">
<div className="hidden shrink-0 xl:block">
<ConnectionDot />
</div>
<div className="flex shrink-0 items-center gap-2">
<LanguageSwitcher />
{user ? (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="flex h-11 w-11 items-center justify-center gap-2 rounded-full border border-[#E6E1DE] bg-white px-1.5 text-sm font-medium text-[#1D1715] transition-colors hover:bg-[#F7F5F4] sm:w-auto sm:justify-start sm:px-2"
aria-label={pickAppText(locale, '打开账号菜单', 'Open account menu')}
>
<Avatar className="h-8 w-8 border border-[#E6E1DE]">
<AvatarFallback className="bg-primary text-xs font-semibold text-primary-foreground">
{userInitial}
</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-2xl font-semibold tracking-tight text-foreground">
{pickAppText(locale, `${user.username},你好!`, `Hi, ${user.username}`)}
</p>
<p className="text-sm text-muted-foreground">
{pickAppText(locale, '当前已登录到你的工作区实例。', 'You are currently signed in to your workspace instance.')}
<span className="hidden max-w-28 truncate sm:block">{user.username}</span>
<ChevronDown className="hidden h-4 w-4 text-muted-foreground sm:block" />
</button>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 rounded-3xl border-border/70 p-0 shadow-2xl">
<div className="overflow-hidden rounded-3xl bg-[linear-gradient(180deg,#F7F5F4,#FFFFFF)]">
<div className="border-b border-border/60 px-6 py-5">
<p className="truncate text-center text-sm font-medium text-muted-foreground">
{user.email}
</p>
</div>
</div>
<div className="border-t border-border/60 bg-white/90 px-4 py-4">
<Button
type="button"
variant="outline"
onClick={handleLogout}
className="h-12 w-full justify-center rounded-2xl text-sm font-semibold"
>
<LogOut className="mr-2 h-4 w-4" />
{pickAppText(locale, '退出登录', 'Sign Out')}
</Button>
<div className="flex flex-col items-center gap-4 px-6 py-6 text-center">
<Avatar className="h-24 w-24 border-4 border-white shadow-sm">
<AvatarFallback className="bg-primary text-4xl font-semibold text-primary-foreground">
{userInitial}
</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-2xl font-semibold tracking-tight text-foreground">
{pickAppText(locale, `${user.username},你好!`, `Hi, ${user.username}`)}
</p>
<p className="text-sm text-muted-foreground">
{pickAppText(locale, '当前已登录到你的工作区实例。', 'You are currently signed in to your workspace instance.')}
</p>
</div>
</div>
<div className="border-t border-border/60 bg-white/90 px-4 py-4">
<Button
type="button"
variant="outline"
onClick={handleLogout}
className="h-12 w-full justify-center rounded-2xl text-sm font-semibold"
>
<LogOut className="mr-2 h-4 w-4" />
{pickAppText(locale, '退出登录', 'Sign Out')}
</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
) : !isAuthLoading ? null : null}
</PopoverContent>
</Popover>
) : !isAuthLoading ? null : null}
</div>
</div>
</div>
</div>
</div>
</header>
</header>
{mobileMenuOpen && (
<>
<button
type="button"
className="fixed inset-x-0 bottom-0 top-16 z-40 bg-black/40 2xl:hidden"
aria-label={pickAppText(locale, '关闭导航', 'Close navigation')}
onClick={() => setMobileMenuOpen(false)}
/>
<nav
id="app-primary-mobile-nav"
aria-label={pickAppText(locale, '主导航', 'Primary navigation')}
className="fixed bottom-0 left-0 top-16 z-[45] isolate w-[min(86vw,320px)] overflow-y-auto border-r border-[#E6E1DE] bg-background text-foreground shadow-[12px_0_32px_rgba(29,23,21,0.24)] animate-in slide-in-from-left-full duration-200 2xl:hidden"
>
<div className="min-h-full bg-background px-4 py-5">
<div className="grid gap-2 bg-background">
{renderNavLinks(true)}
</div>
</div>
</nav>
</>
)}
</>
);
};

View File

@ -27,7 +27,7 @@ export function LanguageSwitcher({ className }: { className?: string }) {
type="button"
onClick={() => setLocale(option.value)}
className={cn(
'rounded px-2 py-1 text-xs font-medium transition-colors',
'h-11 w-11 rounded text-xs font-medium transition-colors',
locale === option.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'

View File

@ -1,254 +1,21 @@
'use client';
import React from 'react';
import {
AlertCircle,
CheckCircle2,
Circle,
FileJson,
FileOutput,
FileText,
Image as ImageIcon,
Link2,
ListChecks,
Loader2,
PanelRightOpen,
X,
} from 'lucide-react';
import { Activity, PanelRightOpen, X } from 'lucide-react';
import { TaskTimeline } from '@/components/task-detail';
import { ScrollArea } from '@/components/ui/scroll-area';
import { appStatusLabel } from '@/lib/i18n/common';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import type {
SessionProgressArtifactView,
SessionProgressStepView,
SessionProgressView,
} from '@/lib/session-progress';
import type { ProcessArtifact, ProcessRunStatus } from '@/types';
function formatShortTime(value: string, locale: 'zh-CN' | 'en-US') {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function statusTone(status: ProcessRunStatus) {
if (status === 'done') return 'text-[#2F8D50] bg-[#E3F1E7] border-[#B8D9C2]';
if (status === 'running') return 'text-[#2F6FCA] bg-[#E7EEF9] border-[#B8CBE8]';
if (status === 'error') return 'text-[#8A3A2D] bg-[#F0E5E1] border-[#D9BDB4]';
if (status === 'cancelled') return 'text-[#6A5E58] bg-[#ECE8E5] border-[#D8D2CE]';
return 'text-[#6A5E58] bg-[#F0ECE9] border-[#D8D2CE]';
}
function StepMarker({ step, index }: { step: SessionProgressStepView; index: number }) {
if (step.status === 'done') {
return (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#2F8D50] text-white">
<CheckCircle2 className="h-4 w-4" />
</span>
);
}
if (step.status === 'running') {
return (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#2F6FCA] text-[11px] font-semibold text-white">
{index + 1}
</span>
);
}
if (step.status === 'error') {
return (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#8A3A2D] text-white">
<AlertCircle className="h-4 w-4" />
</span>
);
}
return (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#D8D2CE] text-[#6A5E58]">
<Circle className="h-3.5 w-3.5" />
</span>
);
}
function artifactIcon(type: ProcessArtifact['artifact_type']) {
if (type === 'json') return <FileJson className="h-4 w-4" />;
if (type === 'image') return <ImageIcon className="h-4 w-4" />;
if (type === 'link') return <Link2 className="h-4 w-4" />;
if (type === 'markdown' || type === 'text') return <FileText className="h-4 w-4" />;
return <FileOutput className="h-4 w-4" />;
}
function ProgressHeader({ view }: { view: SessionProgressView }) {
const { locale } = useAppI18n();
const percent = view.progress.percent;
return (
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4 shadow-[0_8px_24px_rgba(0,0,0,0.04)]">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#E3F1E7] text-[#2F8D50]">
<ListChecks className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<div className="line-clamp-2 text-sm font-semibold text-foreground">{view.title}</div>
<div className="mt-2 flex items-center gap-2">
<span className={`rounded-full border px-2 py-0.5 text-[11px] font-medium ${statusTone(view.status)}`}>
{appStatusLabel(view.status, locale)}
</span>
<span className="text-[11px] text-muted-foreground">
{pickAppText(locale, '更新于', 'Updated')} {formatShortTime(view.updatedAt, locale)}
</span>
</div>
</div>
</div>
<div className="mt-4">
<div className="mb-2 flex items-center justify-between gap-3 text-xs text-muted-foreground">
<span>{view.progress.label}</span>
{percent !== null && <span className="font-medium text-foreground">{percent}%</span>}
</div>
<div className="h-2 overflow-hidden rounded-full bg-[#ECE8E5]">
<div
className="h-full rounded-full bg-[#5DB56F] transition-all"
style={{ width: `${percent ?? 0}%` }}
/>
</div>
</div>
{view.summary && (
<p className="mt-3 line-clamp-3 text-xs leading-5 text-muted-foreground">{view.summary}</p>
)}
</section>
);
}
function StepList({ steps }: { steps: SessionProgressStepView[] }) {
const { locale } = useAppI18n();
return (
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
{pickAppText(locale, '运行步骤', 'Run Steps')}
</h3>
<span className="text-xs text-muted-foreground">
{pickAppText(locale, `${steps.length}`, `${steps.length} steps`)}
</span>
</div>
<div className="space-y-0">
{steps.map((step, index) => (
<div key={step.runId} className="grid grid-cols-[24px_1fr] gap-3">
<div className="flex flex-col items-center">
<StepMarker step={step} index={index} />
{index < steps.length - 1 && <span className="mt-2 h-full min-h-8 w-px bg-[#E6E1DE]" />}
</div>
<div className="pb-5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="line-clamp-2 text-sm font-medium text-foreground">
{index + 1}. {step.title}
</div>
<div className="mt-1 text-[11px] text-muted-foreground">
{step.actorName} · {formatShortTime(step.updatedAt, locale)}
</div>
</div>
<span className={`shrink-0 rounded-full border px-2 py-0.5 text-[11px] ${statusTone(step.status)}`}>
{appStatusLabel(step.status, locale)}
</span>
</div>
{step.description && (
<p className="mt-2 line-clamp-3 text-xs leading-5 text-muted-foreground">
{step.description}
</p>
)}
{step.status === 'running' && (
<div className="mt-2 flex items-center gap-1.5 text-[11px] text-[#2F6FCA]">
<Loader2 className="h-3 w-3 animate-spin" />
<span>{pickAppText(locale, '正在处理', 'In progress')}</span>
</div>
)}
</div>
</div>
))}
</div>
</section>
);
}
function ArtifactRow({ artifact }: { artifact: SessionProgressArtifactView }) {
return (
<a
href={artifact.url || undefined}
target={artifact.url ? '_blank' : undefined}
rel={artifact.url ? 'noreferrer' : undefined}
className="block rounded-lg border border-[#ECE7E3] bg-[#FDFDFC] px-3 py-3 transition-colors hover:bg-[#F7F6F5]"
>
<div className="flex items-start gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#ECE8E5] text-[#5F5550]">
{artifactIcon(artifact.type)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-foreground">{artifact.title}</div>
<div className="mt-1 text-[11px] text-muted-foreground">
{artifact.actorName} · {artifact.typeLabel}
</div>
<p className="mt-2 line-clamp-2 text-xs leading-5 text-muted-foreground">{artifact.preview}</p>
</div>
</div>
</a>
);
}
function ArtifactSection({ view }: { view: SessionProgressView }) {
const { locale } = useAppI18n();
return (
<section className="rounded-lg border border-[#ECE7E3] bg-white px-4 py-4">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
{pickAppText(locale, '生成内容', 'Generated Content')}
</h3>
<span className="text-xs text-muted-foreground">
{pickAppText(locale, `${view.artifacts.length}`, `${view.artifacts.length} items`)}
</span>
</div>
{view.artifactTypeSummaries.length > 0 ? (
<div className="mb-3 flex flex-wrap gap-2">
{view.artifactTypeSummaries.map((item) => (
<span
key={item.type}
className="inline-flex items-center gap-1.5 rounded-full border border-[#E6E1DE] bg-[#F7F6F5] px-2.5 py-1 text-xs text-[#4F4642]"
>
{artifactIcon(item.type)}
<span>{item.label}</span>
<span className="font-semibold">{item.count}</span>
</span>
))}
</div>
) : (
<p className="mb-3 text-xs text-muted-foreground">
{pickAppText(locale, '暂时还没有生成内容。', 'No generated content yet.')}
</p>
)}
<div className="space-y-2">
{view.artifacts.map((artifact) => (
<ArtifactRow key={artifact.artifactId} artifact={artifact} />
))}
</div>
</section>
);
}
import type { TaskTimelineCard } from '@/types';
function ProgressPanel({
view,
cards,
isLive,
onClose,
}: {
view: SessionProgressView;
cards: TaskTimelineCard[];
isLive: boolean;
onClose?: () => void;
}) {
const { locale } = useAppI18n();
@ -260,11 +27,14 @@ function ProgressPanel({
<h2 className="text-base font-semibold text-foreground">
{pickAppText(locale, '当前会话的运行进度', 'Current Session Progress')}
</h2>
<p className="text-xs text-muted-foreground">
{pickAppText(locale, '任务列表会自动刷新', 'Task updates refresh automatically')}
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
{isLive ? <Activity className="h-3.5 w-3.5" /> : null}
{isLive
? pickAppText(locale, '任务时间线实时更新', 'Task timeline updates live')
: pickAppText(locale, '与任务详情时间线一致', 'Matches the Task detail timeline')}
</p>
</div>
{onClose && (
{onClose ? (
<button
type="button"
onClick={onClose}
@ -273,28 +43,32 @@ function ProgressPanel({
>
<X className="h-4 w-4" />
</button>
)}
) : null}
</div>
<ScrollArea className="min-h-0 flex-1 px-4 py-4">
<div className="space-y-4 pb-6">
<ProgressHeader view={view} />
<StepList steps={view.steps} />
<ArtifactSection view={view} />
<div className="pb-6">
<TaskTimeline cards={cards} isLive={isLive} showHeader={false} />
</div>
</ScrollArea>
</div>
);
}
export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressView }) {
export function CurrentSessionProgressSidebar({
cards,
isLive,
}: {
cards: TaskTimelineCard[];
isLive: boolean;
}) {
const { locale } = useAppI18n();
const [mobileOpen, setMobileOpen] = React.useState(false);
return (
<>
<aside className="hidden h-full w-[380px] shrink-0 border-l border-[#E6E1DE] xl:flex">
<ProgressPanel view={view} />
<ProgressPanel cards={cards} isLive={isLive} />
</aside>
<button
@ -306,7 +80,7 @@ export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressV
<PanelRightOpen className="h-5 w-5" />
</button>
{mobileOpen && (
{mobileOpen ? (
<div className="fixed inset-0 z-50 xl:hidden">
<button
type="button"
@ -315,10 +89,10 @@ export function CurrentSessionProgressSidebar({ view }: { view: SessionProgressV
aria-label={pickAppText(locale, '关闭进度面板', 'Close progress panel')}
/>
<div className="absolute inset-y-0 right-0 w-[min(92vw,390px)] border-l border-[#E6E1DE] shadow-2xl">
<ProgressPanel view={view} onClose={() => setMobileOpen(false)} />
<ProgressPanel cards={cards} isLive={isLive} onClose={() => setMobileOpen(false)} />
</div>
</div>
)}
) : null}
</>
);
}

View File

@ -10,6 +10,14 @@ import { getTaskCardMessageIndexes, hasVisibleChatContent, normalizedMessageText
import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock';
import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import { containedPreservedLongTextClass } from '@/lib/text-wrapping';
@ -58,6 +66,8 @@ function MessageBubble({
const textContent = normalizedMessageText(message.content);
const [feedbackMode, setFeedbackMode] = React.useState<'accept' | null>(null);
const [feedbackComment, setFeedbackComment] = React.useState('');
const [confirmAbandonOpen, setConfirmAbandonOpen] = React.useState(false);
const feedbackTextareaId = message.run_id ? `feedback-note-${message.run_id}` : undefined;
return (
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
@ -130,7 +140,7 @@ function MessageBubble({
</div>
<Link
href={`/tasks/${encodeURIComponent(message.task_id)}`}
className="inline-flex h-8 items-center gap-1 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
className="inline-flex h-11 items-center gap-1 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
{pickAppText(locale, '查看任务', 'Open task')}
<ChevronRight className="h-3.5 w-3.5" />
@ -157,7 +167,7 @@ function MessageBubble({
<button
type="button"
onClick={() => setFeedbackMode('accept')}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ThumbsUp className="h-3.5 w-3.5" />
{pickAppText(locale, '接受', 'Accept')}
@ -165,15 +175,15 @@ function MessageBubble({
<button
type="button"
onClick={() => onRequestRevision(message.run_id!)}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<RefreshCcw className="h-3.5 w-3.5" />
{pickAppText(locale, '需要修改', 'Revise')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, 'abandon')}
className="inline-flex h-8 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
onClick={() => setConfirmAbandonOpen(true)}
className="inline-flex h-11 items-center gap-1 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
>
<XCircle className="h-3.5 w-3.5" />
{pickAppText(locale, '放弃', 'Abandon')}
@ -181,7 +191,11 @@ function MessageBubble({
</div>
{feedbackMode && (
<div className="space-y-2 rounded-md border border-border bg-background p-2">
<label htmlFor={feedbackTextareaId} className="sr-only">
{pickAppText(locale, '接受反馈备注', 'Acceptance note')}
</label>
<textarea
id={feedbackTextareaId}
value={feedbackComment}
onChange={(event) => setFeedbackComment(event.target.value)}
placeholder={pickAppText(locale, '可选:补充说明...', 'Optional note...')}
@ -194,14 +208,14 @@ function MessageBubble({
setFeedbackMode(null);
setFeedbackComment('');
}}
className="h-8 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent"
className="h-11 rounded-md border border-border px-3 text-xs text-muted-foreground hover:bg-accent"
>
{pickAppText(locale, '取消', 'Cancel')}
</button>
<button
type="button"
onClick={() => onFeedback(message.run_id!, feedbackMode, feedbackComment.trim() || undefined)}
className="h-8 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
className="h-11 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
{pickAppText(locale, '提交', 'Submit')}
</button>
@ -210,6 +224,35 @@ function MessageBubble({
)}
</>
)}
<Dialog open={confirmAbandonOpen} onOpenChange={setConfirmAbandonOpen}>
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-md">
<DialogHeader>
<DialogTitle>{pickAppText(locale, '放弃当前任务?', 'Abandon this task?')}</DialogTitle>
<DialogDescription>
{pickAppText(locale, '放弃后会停止等待该任务的验收结果,此操作需要明确确认。', 'This stops waiting for this task acceptance result and requires confirmation.')}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-2">
<button
type="button"
onClick={() => setConfirmAbandonOpen(false)}
className="h-11 rounded-md border border-border px-4 text-sm text-muted-foreground hover:bg-accent"
>
{pickAppText(locale, '取消', 'Cancel')}
</button>
<button
type="button"
onClick={() => {
setConfirmAbandonOpen(false);
onFeedback(message.run_id!, 'abandon');
}}
className="h-11 rounded-md bg-destructive px-4 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
>
{pickAppText(locale, '确认放弃', 'Confirm abandon')}
</button>
</DialogFooter>
</DialogContent>
</Dialog>
{message.feedback_error && (
<span className="text-xs text-destructive">{message.feedback_error}</span>
)}
@ -394,8 +437,8 @@ export function MessageList({
})();
return (
<ScrollArea className="h-full px-8" viewportRef={viewportRef}>
<div className="mx-auto max-w-5xl space-y-8 py-10">
<ScrollArea className="h-full px-3 sm:px-5 md:px-8" viewportRef={viewportRef}>
<div className="mx-auto max-w-5xl space-y-8 py-6 md:py-10">
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<Bot className="w-12 h-12 mb-4 opacity-50" />

View File

@ -54,27 +54,27 @@ export function SkillDetailView({
}: SkillDetailViewProps) {
const readme = stripFrontmatter(content || '');
return (
<div className="rounded-lg border border-border bg-white text-black [--background:0_0%_100%] [--card:0_0%_100%] [--card-foreground:0_0%_0%] [--foreground:0_0%_0%] [--muted-foreground:0_0%_0%] [--popover:0_0%_100%] [--popover-foreground:0_0%_0%] [--secondary-foreground:0_0%_0%]">
<div className="border-b border-border p-5">
<div className="min-w-0 rounded-lg border border-border bg-white text-black [--background:0_0%_100%] [--card:0_0%_100%] [--card-foreground:0_0%_0%] [--foreground:0_0%_0%] [--muted-foreground:0_0%_0%] [--popover:0_0%_100%] [--popover-foreground:0_0%_0%] [--secondary-foreground:0_0%_0%]">
<div className="border-b border-border p-4 sm:p-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
{badges}
<Badge variant="outline">v{currentVersion || '-'}</Badge>
{loadingVersion && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
</div>
<h2 className="break-words text-2xl font-semibold tracking-normal">{title}</h2>
{summary && <p className="mt-2 max-w-4xl text-sm leading-6 text-muted-foreground">{summary}</p>}
{summary && <p className="mt-2 max-w-4xl break-words text-sm leading-6 text-muted-foreground">{summary}</p>}
</div>
{actions}
</div>
</div>
<Tabs defaultValue="overview" className="p-5">
<TabsList>
<TabsTrigger value="overview">{labels.overview}</TabsTrigger>
<TabsTrigger value="files">{labels.files}</TabsTrigger>
<TabsTrigger value="versions">{labels.versions}</TabsTrigger>
<Tabs defaultValue="overview" className="p-4 sm:p-5">
<TabsList className="h-auto min-h-11 flex-wrap">
<TabsTrigger value="overview" className="min-h-11">{labels.overview}</TabsTrigger>
<TabsTrigger value="files" className="min-h-11">{labels.files}</TabsTrigger>
<TabsTrigger value="versions" className="min-h-11">{labels.versions}</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-5">
@ -86,7 +86,7 @@ export function SkillDetailView({
</TabsContent>
<TabsContent value="files" className="mt-5">
<div className="grid gap-4 lg:grid-cols-[320px_minmax(0,1fr)]">
<div className="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,320px)_minmax(0,1fr)]">
<div className="min-h-[420px] rounded-lg border border-border">
{files.length === 0 ? (
<EmptyPanel icon={<FileText className="h-7 w-7" />} text={labels.noFiles} />
@ -96,7 +96,7 @@ export function SkillDetailView({
<button
key={file.filePath}
type="button"
className={`flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm transition hover:bg-muted ${
className={`flex min-h-11 w-full items-center justify-between gap-3 rounded-md px-3 py-3 text-left text-sm transition hover:bg-muted ${
selectedFile?.filePath === file.filePath ? 'bg-muted' : ''
}`}
onClick={() => onOpenFile(file.filePath)}
@ -109,7 +109,7 @@ export function SkillDetailView({
)}
</div>
<div className="min-h-[420px] rounded-lg border border-border bg-muted/20 p-4">
<div className="min-w-0 rounded-lg border border-border bg-muted/20 p-4 lg:min-h-[420px]">
{loadingFile ? (
<div className="flex h-[360px] items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
@ -129,7 +129,7 @@ export function SkillDetailView({
<button
key={version.version}
type="button"
className={`flex w-full items-center justify-between gap-4 rounded-lg border px-4 py-3 text-left transition hover:bg-muted ${
className={`flex min-h-11 w-full flex-col gap-3 rounded-lg border px-4 py-3 text-left transition hover:bg-muted sm:flex-row sm:items-center sm:justify-between sm:gap-4 ${
version.version === currentVersion ? 'border-primary bg-muted' : 'border-border'
}`}
onClick={() => onSelectVersion(version.version)}
@ -137,15 +137,15 @@ export function SkillDetailView({
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="font-mono text-sm">{version.version}</span>
<span className="break-all font-mono text-sm">{version.version}</span>
{version.version === currentVersion && <Badge variant="secondary">{labels.current}</Badge>}
{version.status && <Badge variant="outline">{version.status}</Badge>}
</div>
{version.changeReason && (
<p className="mt-1 truncate text-xs text-muted-foreground">{version.changeReason}</p>
<p className="mt-1 break-words text-xs text-muted-foreground">{version.changeReason}</p>
)}
</div>
<div className="shrink-0 text-right text-xs text-muted-foreground">
<div className="shrink-0 text-left text-xs text-muted-foreground sm:text-right">
{version.publishedAt || version.createdAt || ''}
{typeof version.totalSize === 'number' && (
<div>
@ -165,7 +165,7 @@ export function SkillDetailView({
function FilePreview({ file, labels }: { file: SkillFileContent; labels: SkillDetailViewProps['labels'] }) {
const content = file.content || '';
return (
<div className="space-y-3">
<div className="min-w-0 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="break-all font-mono text-sm font-medium">{file.filePath}</div>
<Badge variant="outline">
@ -177,7 +177,7 @@ function FilePreview({ file, labels }: { file: SkillFileContent; labels: SkillDe
) : isMarkdown(file.filePath, file.contentType) ? (
<MarkdownPreview content={stripFrontmatter(content)} />
) : (
<pre className="max-h-[560px] overflow-auto rounded-md border border-border bg-background p-4 text-xs leading-5">
<pre className="max-h-[560px] max-w-full overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-background p-4 text-xs leading-5">
{content}
</pre>
)}
@ -187,7 +187,7 @@ function FilePreview({ file, labels }: { file: SkillFileContent; labels: SkillDe
function MarkdownPreview({ content }: { content: string }) {
return (
<div className="prose prose-sm max-w-none text-black prose-a:text-black prose-blockquote:text-black prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-black prose-headings:text-black prose-headings:tracking-normal prose-li:text-black prose-p:text-black prose-pre:border prose-pre:border-border prose-pre:bg-muted prose-pre:text-black prose-strong:text-black prose-table:text-black prose-td:text-black prose-th:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<div className="prose prose-sm max-w-none break-words text-black prose-a:text-black prose-blockquote:text-black prose-code:break-words prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-black prose-headings:text-black prose-headings:tracking-normal prose-li:text-black prose-p:text-black prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:border prose-pre:border-border prose-pre:bg-muted prose-pre:text-black prose-strong:text-black prose-table:block prose-table:overflow-x-auto prose-table:text-black prose-td:text-black prose-th:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
);

View File

@ -7,6 +7,7 @@ import { TaskRuntimeStatusBadge, formatTaskRuntimeTime } from '@/components/task
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
@ -96,7 +97,7 @@ function FeedbackButton({
const isBusy = actionBusy === type || Boolean(actionBusy?.endsWith(type));
return (
<Button type="button" variant="outline" className="w-full justify-center" disabled={disabled || Boolean(actionBusy)} onClick={onClick}>
<Button type="button" variant="outline" className="h-11 w-full justify-center" disabled={disabled || Boolean(actionBusy)} onClick={onClick}>
{isBusy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : icon}
{label}
</Button>
@ -156,6 +157,7 @@ export function TaskAcceptanceControls({
onSubmit,
}: Props) {
const { locale } = useAppI18n();
const commentId = React.useId();
const [localComment, setLocalComment] = React.useState('');
const comment = revision ?? localComment;
const setComment = onRevisionChange ?? setLocalComment;
@ -224,12 +226,16 @@ export function TaskAcceptanceControls({
/>
</div>
<Textarea
value={comment}
onChange={(event) => setComment(event.target.value)}
disabled={Boolean(recordedFeedback) || isFinalized || !isReadyForAcceptance || Boolean(actionBusy)}
placeholder={pickAppText(locale, '需要修改时写下具体要求;接受或放弃可选填说明。', 'Describe requested changes; notes are optional for accept or abandon.')}
/>
<div className="space-y-2">
<Label htmlFor={commentId}>{pickAppText(locale, '验收说明', 'Acceptance note')}</Label>
<Textarea
id={commentId}
value={comment}
onChange={(event) => setComment(event.target.value)}
disabled={Boolean(recordedFeedback) || isFinalized || !isReadyForAcceptance || Boolean(actionBusy)}
placeholder={pickAppText(locale, '需要修改时写下具体要求;接受或放弃可选填说明。', 'Describe requested changes; notes are optional for accept or abandon.')}
/>
</div>
<div className={`text-xs text-muted-foreground ${containedPreservedLongTextClass}`}>
{pickAppText(locale, '验收将记录到当前任务运行:', 'Acceptance will be recorded on run: ')}
<span className="font-mono">{runId || '-'}</span>

View File

@ -43,17 +43,17 @@ export function TaskLiveHeader({ task, activeLabel, durationMs, reviewTargetId }
const showReviewLink = Boolean(reviewTargetId && ['awaiting_acceptance', 'needs_revision'].includes(task.status));
return (
<header className="sticky top-0 z-20 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<header className="sticky top-[65px] z-20 min-w-0 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="mx-auto flex max-w-7xl flex-col gap-3 px-4 py-3 sm:px-6">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Button asChild variant="outline" size="sm">
<Button asChild variant="outline" size="sm" className="h-11">
<Link href="/tasks">
<ArrowLeft className="mr-2 h-4 w-4" />
{pickAppText(locale, '返回任务', 'Back to tasks')}
</Link>
</Button>
<Button asChild variant="ghost" size="sm">
<Button asChild variant="ghost" size="sm" className="h-11">
<Link href="/">
<MessageSquare className="mr-2 h-4 w-4" />
{pickAppText(locale, '对话', 'Chat')}
@ -70,7 +70,7 @@ export function TaskLiveHeader({ task, activeLabel, durationMs, reviewTargetId }
)}
{activeLabel ? <Badge variant="secondary">{activeLabel}</Badge> : null}
{showReviewLink ? (
<Button asChild variant="default" size="sm">
<Button asChild variant="default" size="sm" className="h-11">
<a href={`#${reviewTargetId}`}>
<CheckCircle2 className="mr-2 h-4 w-4" />
{pickAppText(locale, '验收', 'Review')}

View File

@ -131,7 +131,7 @@ export function TaskSideRail({ task, runs, artifacts, cards }: Props) {
const latestAlert = cards.filter(isWarningOrError).sort((a, b) => toTime(b.createdAt) - toTime(a.createdAt))[0] ?? null;
return (
<aside className="space-y-4">
<aside className="min-w-0 space-y-4">
<Card className="rounded-md">
<CardHeader>
<CardTitle className="text-base">{pickAppText(locale, '任务状态', 'Task status')}</CardTitle>
@ -230,14 +230,14 @@ export function TaskSideRail({ task, runs, artifacts, cards }: Props) {
<div className="mt-1 text-xs text-muted-foreground">{artifact.artifact_type}</div>
</div>
{href ? (
<Button asChild size="sm" variant="outline" className="shrink-0">
<Button asChild size="sm" variant="outline" className="h-11 shrink-0">
<a href={href} target="_blank" rel="noopener noreferrer">
{artifact.url ? <ExternalLink className="mr-2 h-3.5 w-3.5" /> : <Download className="mr-2 h-3.5 w-3.5" />}
{artifact.url ? pickAppText(locale, '打开', 'Open') : pickAppText(locale, '下载', 'Download')}
</a>
</Button>
) : inlinePayload ? (
<Button size="sm" variant="outline" className="shrink-0" onClick={() => downloadInlineArtifact(artifact)}>
<Button size="sm" variant="outline" className="h-11 shrink-0" onClick={() => downloadInlineArtifact(artifact)}>
<Download className="mr-2 h-3.5 w-3.5" />
{pickAppText(locale, '下载', 'Download')}
</Button>

View File

@ -14,26 +14,29 @@ type Props = {
isLive: boolean;
resultAcceptance?: TaskResultAcceptance;
reviewTargetId?: string;
showHeader?: boolean;
};
export function TaskTimeline({ cards, isLive, resultAcceptance, reviewTargetId }: Props) {
export function TaskTimeline({ cards, isLive, resultAcceptance, reviewTargetId, showHeader = true }: Props) {
const { locale } = useAppI18n();
return (
<section className="space-y-3">
<div className="flex items-center justify-between gap-3">
<h2 className="text-base font-semibold">{pickAppText(locale, '时间线', 'Timeline')}</h2>
{isLive ? (
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span>
<Activity className="h-3.5 w-3.5" />
{pickAppText(locale, '实时更新', 'Live')}
</div>
) : null}
</div>
{showHeader ? (
<div className="flex items-center justify-between gap-3">
<h2 className="text-base font-semibold">{pickAppText(locale, '时间线', 'Timeline')}</h2>
{isLive ? (
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span>
<Activity className="h-3.5 w-3.5" />
{pickAppText(locale, '实时更新', 'Live')}
</div>
) : null}
</div>
) : null}
{cards.length === 0 ? (
<Card className="rounded-md border-dashed">

View File

@ -148,7 +148,7 @@ function TaskResultHistory({ card }: { card: TaskTimelineCardView }) {
return (
<details className="mt-3 min-w-0 max-w-full overflow-hidden rounded-md border border-border bg-muted/20 px-3 py-2 text-sm">
<summary className="flex cursor-pointer select-none items-center justify-between gap-3 font-medium">
<summary className="flex min-h-[44px] cursor-pointer select-none items-center justify-between gap-3 font-medium">
<span>{pickAppText(locale, '展开历史版本', 'Show previous versions')}</span>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</summary>
@ -182,7 +182,7 @@ export function TaskTimelineCard({ card, resultAcceptance, reviewTargetId }: Pro
const shouldRenderResultAcceptance = Boolean(card.type === 'result' && resultAcceptance && card.runId === resultAcceptance.runId);
return (
<Card id={shouldRenderResultAcceptance ? reviewTargetId : undefined} className="scroll-mt-28 overflow-hidden rounded-md">
<Card id={shouldRenderResultAcceptance ? reviewTargetId : undefined} className="min-w-0 max-w-full scroll-mt-44 overflow-hidden rounded-md">
<CardContent className="p-4">
<div className="flex gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted">
@ -224,7 +224,7 @@ export function TaskTimelineCard({ card, resultAcceptance, reviewTargetId }: Pro
{card.type === 'result_history' ? <TaskResultHistory card={card} /> : card.details ? (
<details className="mt-3 min-w-0 max-w-full overflow-hidden rounded-md border border-border bg-muted/20 px-3 py-2 text-xs">
<summary className="cursor-pointer select-none font-medium text-muted-foreground">
<summary className="flex min-h-[44px] cursor-pointer select-none items-center font-medium text-muted-foreground">
{pickAppText(locale, '详情 JSON', 'Details JSON')}
</summary>
<pre className={`mt-2 max-h-72 overflow-auto text-[11px] leading-5 text-muted-foreground ${containedJsonTextClass}`}>

View File

@ -41,7 +41,7 @@ export function TaskManagementTabs() {
key={tab.href}
href={tab.href}
className={cn(
'inline-flex items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-colors',
'inline-flex h-11 items-center gap-2 rounded-xl px-4 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'

View File

@ -20,10 +20,10 @@ const buttonVariants = cva(
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
default: 'h-11 px-4 py-2',
sm: 'h-11 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
icon: 'h-11 w-11',
},
},
defaultVariants: {

View File

@ -38,13 +38,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-4 right-4 top-4 z-50 grid max-h-[calc(100vh-2rem)] w-auto max-w-none gap-4 overflow-x-hidden overflow-y-auto rounded-lg border bg-background p-5 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 sm:left-[50%] sm:right-auto sm:top-[50%] sm:w-full sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:p-6 sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%] [&>*]:min-w-0',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-3 top-3 flex h-11 w-11 items-center justify-center rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>

View File

@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-lg border border-transparent bg-input px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/10 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-11 w-full rounded-lg border border-transparent bg-input px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/10 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}

View File

@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
'flex h-11 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}

View File

@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
'peer inline-flex h-11 w-14 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent px-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-7 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>

View File

@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
'inline-flex min-h-11 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className
)}
{...props}
@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
'inline-flex h-11 min-h-11 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className
)}
{...props}