feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
@ -78,8 +78,7 @@ export function AppRuntimeBridge() {
|
||||
|
||||
React.useEffect(() => {
|
||||
resetProcessState();
|
||||
const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId;
|
||||
wsManager.connect(wsSessionId);
|
||||
wsManager.connect(sessionId);
|
||||
}, [resetProcessState, sessionId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
21
app-instance/frontend/components/AppShell.tsx
Normal file
21
app-instance/frontend/components/AppShell.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import Header from '@/components/Header';
|
||||
import AuthGuard from '@/components/AuthGuard';
|
||||
import { AppRuntimeBridge } from '@/components/AppRuntimeBridge';
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<Header />
|
||||
<main className="pt-16">
|
||||
<AuthGuard>
|
||||
<AppRuntimeBridge />
|
||||
{children}
|
||||
</AuthGuard>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,9 +2,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { MessageSquare, Activity, Clock, Puzzle, Blocks, FolderOpen, Store, LogIn, UserPlus, Bot, ServerCog, Mail, LogOut, ChevronDown } from 'lucide-react';
|
||||
import { Bell, Bot, ChevronDown, ListTodo, LogOut, Mail, MessageSquare, PackageOpen, Puzzle, Settings, Store, Wrench } from 'lucide-react';
|
||||
import { logout } from '@/lib/api';
|
||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
@ -16,17 +15,7 @@ import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
|
||||
type NavItem = {
|
||||
key:
|
||||
| 'chat'
|
||||
| 'status'
|
||||
| 'office'
|
||||
| 'skills'
|
||||
| 'plugins'
|
||||
| 'agents'
|
||||
| 'mcp'
|
||||
| 'outlook'
|
||||
| 'marketplace'
|
||||
| 'files';
|
||||
key: 'chat' | 'tasks' | 'notifications' | 'skills' | 'tools' | 'agents' | 'outlook' | 'marketplace' | 'plugins' | 'settings';
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
matchPrefixes?: string[];
|
||||
@ -34,22 +23,22 @@ type NavItem = {
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'chat', href: '/', icon: MessageSquare },
|
||||
{ key: 'status', href: '/status', icon: Activity },
|
||||
{ key: 'office', href: '/office', icon: Clock, matchPrefixes: ['/office', '/cron'] },
|
||||
{ key: 'tasks', href: '/tasks', icon: ListTodo, matchPrefixes: ['/tasks', '/office', '/cron'] },
|
||||
{ key: 'notifications', href: '/notifications', icon: Bell, matchPrefixes: ['/notifications'] },
|
||||
{ key: 'skills', href: '/skills', icon: Puzzle },
|
||||
{ key: 'plugins', href: '/plugins', icon: Blocks },
|
||||
{ key: 'agents', href: '/agents', icon: Bot },
|
||||
{ key: 'mcp', href: '/mcp', icon: ServerCog },
|
||||
{ key: 'outlook', href: '/outlook', icon: Mail },
|
||||
{ key: 'marketplace', href: '/marketplace', icon: Store },
|
||||
{ key: 'files', href: '/files', icon: FolderOpen },
|
||||
{ key: 'tools', href: '/mcp', icon: Wrench, matchPrefixes: ['/mcp'] },
|
||||
{ key: 'agents', href: '/agents', icon: Bot, matchPrefixes: ['/agents'] },
|
||||
{ key: 'outlook', href: '/outlook', icon: Mail, matchPrefixes: ['/outlook'] },
|
||||
{ key: 'marketplace', href: '/marketplace', icon: Store, matchPrefixes: ['/marketplace'] },
|
||||
{ key: 'plugins', href: '/plugins', icon: PackageOpen, matchPrefixes: ['/plugins'] },
|
||||
{
|
||||
key: 'settings',
|
||||
href: '/settings',
|
||||
icon: Settings,
|
||||
matchPrefixes: ['/settings', '/status', '/logs'],
|
||||
},
|
||||
];
|
||||
|
||||
const AUTH_ITEMS = [
|
||||
{ key: 'login', href: '/login', icon: LogIn },
|
||||
{ key: 'register', href: '/register', icon: UserPlus },
|
||||
] as const;
|
||||
|
||||
function ConnectionDot() {
|
||||
const { locale } = useAppI18n();
|
||||
const wsStatus = useChatStore((s) => s.wsStatus);
|
||||
@ -61,10 +50,10 @@ function ConnectionDot() {
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
|
||||
|
||||
const color = isOnline
|
||||
? 'bg-green-500'
|
||||
? 'bg-[#869683]'
|
||||
: isConnecting
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500';
|
||||
? 'bg-[#8B7E77]'
|
||||
: 'bg-[#5F5550]';
|
||||
|
||||
const label = appConnectionStatusLabel(wsStatus, nanobotReady, locale);
|
||||
|
||||
@ -86,23 +75,17 @@ const Header = () => {
|
||||
|
||||
const navLabel = React.useCallback((key: NavItem['key']) => {
|
||||
if (key === 'chat') return pickAppText(locale, '对话', 'Chat');
|
||||
if (key === 'status') return pickAppText(locale, '状态', 'Status');
|
||||
if (key === 'office') return pickAppText(locale, '任务管理', 'Tasks');
|
||||
if (key === 'tasks') return 'Task';
|
||||
if (key === 'notifications') return pickAppText(locale, '通知', 'Notifications');
|
||||
if (key === 'skills') return pickAppText(locale, '技能', 'Skills');
|
||||
if (key === 'plugins') return pickAppText(locale, '插件', 'Plugins');
|
||||
if (key === 'tools') return pickAppText(locale, '工具', 'Tools');
|
||||
if (key === 'agents') return pickAppText(locale, '智能体', 'Agents');
|
||||
if (key === 'mcp') return 'MCP';
|
||||
if (key === 'outlook') return 'Outlook';
|
||||
if (key === 'marketplace') return pickAppText(locale, '市场', 'Marketplace');
|
||||
return pickAppText(locale, '文件', 'Files');
|
||||
if (key === 'plugins') return pickAppText(locale, '插件', 'Plugins');
|
||||
return pickAppText(locale, '配置', 'Settings');
|
||||
}, [locale]);
|
||||
|
||||
const authLabel = React.useCallback((key: 'login' | 'register') => (
|
||||
key === 'login'
|
||||
? pickAppText(locale, '登录', 'Sign In')
|
||||
: pickAppText(locale, '注册', 'Sign Up')
|
||||
), [locale]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
setUser(null);
|
||||
@ -113,24 +96,16 @@ const Header = () => {
|
||||
const userInitial = (user?.username || user?.email || '?').trim().charAt(0).toUpperCase();
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 bg-background border-b border-border z-50">
|
||||
<div className="max-w-[1720px] mx-auto px-5 sm:px-6 lg:px-8 xl:px-10">
|
||||
<div className="flex items-center h-16 gap-6">
|
||||
<Link href="/" className="flex shrink-0 items-center gap-3 pr-2">
|
||||
<Image
|
||||
src="/boardware-logo.jpg"
|
||||
alt="Boardware logo"
|
||||
width={40}
|
||||
height={32}
|
||||
className="h-8 w-10 shrink-0 rounded-sm bg-white object-contain p-0.5"
|
||||
/>
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-semibold leading-none tracking-tight sm:text-[1.15rem]">
|
||||
Boardware Agent Sandbox
|
||||
<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>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-3">
|
||||
<nav className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<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 === '/'
|
||||
@ -141,10 +116,10 @@ const Header = () => {
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
|
||||
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-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
: 'text-[#4F4642] hover:bg-[#F7F5F4] hover:text-[#0B0B0B]'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
@ -154,26 +129,30 @@ const Header = () => {
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2 border-l border-border pl-4">
|
||||
<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-border/70 bg-background px-2 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
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-border/60">
|
||||
<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" />
|
||||
</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-gradient-to-b from-slate-50 via-slate-50 to-white">
|
||||
<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}
|
||||
@ -210,30 +189,7 @@ const Header = () => {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : !isAuthLoading ? (
|
||||
AUTH_ITEMS.map((item) => {
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{authLabel(item.key)}
|
||||
</Link>
|
||||
);
|
||||
})
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-l border-border pl-4">
|
||||
<ConnectionDot />
|
||||
) : !isAuthLoading ? null : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -25,28 +25,28 @@ const TERMINAL_STATUSES = new Set<ProcessRun['status']>(['done', 'error', 'cance
|
||||
|
||||
const AGENT_ACCENTS = [
|
||||
{
|
||||
frame: 'border-sky-500/25 bg-sky-500/[0.05]',
|
||||
title: 'text-sky-300',
|
||||
dot: 'bg-sky-400',
|
||||
result: 'border-sky-500/25 bg-sky-500/[0.08]',
|
||||
frame: 'border-[#BCC4CE] bg-[#E4E7EB]/45',
|
||||
title: 'text-[#697281]',
|
||||
dot: 'bg-[#8C96A3]',
|
||||
result: 'border-[#BCC4CE] bg-[#E4E7EB]/55',
|
||||
},
|
||||
{
|
||||
frame: 'border-emerald-500/25 bg-emerald-500/[0.05]',
|
||||
title: 'text-emerald-300',
|
||||
dot: 'bg-emerald-400',
|
||||
result: 'border-emerald-500/25 bg-emerald-500/[0.08]',
|
||||
frame: 'border-[#B7C2B5] bg-[#E3E8E2]/45',
|
||||
title: 'text-[#657162]',
|
||||
dot: 'bg-[#869683]',
|
||||
result: 'border-[#B7C2B5] bg-[#E3E8E2]/55',
|
||||
},
|
||||
{
|
||||
frame: 'border-amber-500/25 bg-amber-500/[0.05]',
|
||||
title: 'text-amber-300',
|
||||
dot: 'bg-amber-400',
|
||||
result: 'border-amber-500/25 bg-amber-500/[0.08]',
|
||||
frame: 'border-[#B8AEA8] bg-[#E7E2DE]/55',
|
||||
title: 'text-[#5F5550]',
|
||||
dot: 'bg-[#8B7E77]',
|
||||
result: 'border-[#B8AEA8] bg-[#E7E2DE]/65',
|
||||
},
|
||||
{
|
||||
frame: 'border-fuchsia-500/25 bg-fuchsia-500/[0.05]',
|
||||
title: 'text-fuchsia-300',
|
||||
dot: 'bg-fuchsia-400',
|
||||
result: 'border-fuchsia-500/25 bg-fuchsia-500/[0.08]',
|
||||
frame: 'border-[#D8D2CE] bg-[#ECE8E5]/70',
|
||||
title: 'text-[#4F4642]',
|
||||
dot: 'bg-[#6A5E58]',
|
||||
result: 'border-[#D8D2CE] bg-[#ECE8E5]/80',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@ -55,12 +55,12 @@ function accentFor(index: number) {
|
||||
}
|
||||
|
||||
function statusTone(status: ProcessRun['status']) {
|
||||
if (status === 'done') return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300';
|
||||
if (status === 'error') return 'border-rose-500/20 bg-rose-500/10 text-rose-300';
|
||||
if (status === 'cancelled') return 'border-zinc-500/20 bg-zinc-500/10 text-zinc-300';
|
||||
if (status === 'waiting') return 'border-amber-500/20 bg-amber-500/10 text-amber-300';
|
||||
if (status === 'queued') return 'border-sky-500/20 bg-sky-500/10 text-sky-300';
|
||||
return 'border-sky-500/20 bg-sky-500/10 text-sky-300';
|
||||
if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]';
|
||||
if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]';
|
||||
if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]';
|
||||
if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]';
|
||||
if (status === 'queued') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#4F4642]';
|
||||
return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]';
|
||||
}
|
||||
|
||||
function feedTone(role: AgentFeedItem['role']) {
|
||||
@ -166,8 +166,8 @@ function SkillChips({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
const rawEphemeral = metadata?.ephemeral_skill_names;
|
||||
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
|
||||
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
|
||||
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
|
||||
const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@ -182,9 +182,9 @@ function SkillChips({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
ephemeral:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{draftId && (
|
||||
{guidanceId && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
draft:{draftId.slice(0, 8)}
|
||||
guidance:{guidanceId.slice(0, 8)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@ -390,7 +390,7 @@ function ResultCard({
|
||||
<div className="text-[10px] font-medium uppercase tracking-[0.18em] text-muted-foreground">{pickAppText(locale, '结果', 'Result')}</div>
|
||||
<div className={cn('mt-1 truncate text-sm font-semibold', accent.title)}>{run.actor_name}</div>
|
||||
</div>
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
<CheckCircle2 className="h-4 w-4 text-[#657162]" />
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-3 text-sm text-foreground/80">{summary}</div>
|
||||
<div className="mt-3 flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
|
||||
@ -28,7 +28,7 @@ function renderArtifactBody(artifact: ProcessArtifact, locale: 'zh-CN' | 'en-US'
|
||||
}
|
||||
if (artifact.artifact_type === 'link' && artifact.url) {
|
||||
return (
|
||||
<a href={artifact.url} target="_blank" rel="noreferrer" className="text-sm text-sky-300 underline break-all">
|
||||
<a href={artifact.url} target="_blank" rel="noreferrer" className="text-sm text-[#5F5550] underline break-all">
|
||||
{artifact.url}
|
||||
</a>
|
||||
);
|
||||
|
||||
@ -3,12 +3,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { MessageList } from '@/components/chat-workbench/MessageList';
|
||||
import { ArtifactSidebar } from '@/components/chat-workbench/ArtifactSidebar';
|
||||
import { ProcessLane } from '@/components/chat-workbench/ProcessLane';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
|
||||
export function ChatWorkbench({
|
||||
messages,
|
||||
@ -33,58 +28,10 @@ export function ChatWorkbench({
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const [isDesktop, setIsDesktop] = React.useState(() =>
|
||||
typeof window === 'undefined' ? true : window.matchMedia('(min-width: 1024px)').matches
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||
const updateLayout = () => setIsDesktop(mediaQuery.matches);
|
||||
updateLayout();
|
||||
|
||||
if (typeof mediaQuery.addEventListener === 'function') {
|
||||
mediaQuery.addEventListener('change', updateLayout);
|
||||
return () => mediaQuery.removeEventListener('change', updateLayout);
|
||||
}
|
||||
|
||||
mediaQuery.addListener(updateLayout);
|
||||
return () => mediaQuery.removeListener(updateLayout);
|
||||
}, []);
|
||||
|
||||
const selectedRun = selectedRunId
|
||||
? processRuns.find((item) => item.run_id === selectedRunId) || null
|
||||
: null;
|
||||
const selectedRunEvents = selectedRun
|
||||
? processEvents.filter((item) => item.run_id === selectedRun.run_id)
|
||||
: [];
|
||||
const selectedRunArtifacts = selectedRun
|
||||
? processArtifacts.filter((item) => item.run_id === selectedRun.run_id)
|
||||
: [];
|
||||
const hasResultsPanel = Boolean(
|
||||
selectedRun &&
|
||||
(
|
||||
selectedRun.summary ||
|
||||
selectedRunEvents.length > 0 ||
|
||||
selectedRunArtifacts.length > 0
|
||||
)
|
||||
);
|
||||
const hasProcessPanel = processRuns.length > 0;
|
||||
const desktopColumns = hasProcessPanel && hasResultsPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_340px_360px]'
|
||||
: hasProcessPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_340px]'
|
||||
: hasResultsPanel
|
||||
? 'grid-cols-[minmax(0,1fr)_360px]'
|
||||
: 'grid-cols-[minmax(0,1fr)]';
|
||||
|
||||
const messageList = (
|
||||
return (
|
||||
<div className="h-full">
|
||||
<MessageList
|
||||
messages={messages}
|
||||
isThinking={isThinking}
|
||||
@ -93,81 +40,11 @@ export function ChatWorkbench({
|
||||
processRuns={processRuns}
|
||||
processEvents={processEvents}
|
||||
processArtifacts={processArtifacts}
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
onFeedback={onFeedback}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<div className={`grid h-full ${desktopColumns}`}>
|
||||
<div className="min-h-0">
|
||||
{messageList}
|
||||
</div>
|
||||
{hasProcessPanel && (
|
||||
<div className="min-h-0">
|
||||
<ProcessLane
|
||||
runs={processRuns}
|
||||
events={processEvents}
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasResultsPanel && (
|
||||
<div className="min-h-0">
|
||||
<ArtifactSidebar
|
||||
selectedRun={selectedRun}
|
||||
events={processEvents}
|
||||
artifacts={processArtifacts}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
{!hasResultsPanel && !hasProcessPanel ? (
|
||||
messageList
|
||||
) : (
|
||||
<Tabs defaultValue="chat" className="h-full flex flex-col">
|
||||
<div className="px-4 pt-3 border-b border-border">
|
||||
<TabsList className={`grid w-full ${hasResultsPanel ? 'grid-cols-3' : 'grid-cols-2'}`}>
|
||||
<TabsTrigger value="chat">{pickAppText(locale, '聊天', 'Chat')}</TabsTrigger>
|
||||
<TabsTrigger value="process">{pickAppText(locale, '过程', 'Process')}</TabsTrigger>
|
||||
{hasResultsPanel && (
|
||||
<TabsTrigger value="results">{pickAppText(locale, '结果', 'Results')}</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="chat" className="flex-1 min-h-0 mt-0">
|
||||
{messageList}
|
||||
</TabsContent>
|
||||
<TabsContent value="process" className="flex-1 min-h-0 mt-0">
|
||||
<ProcessLane
|
||||
runs={processRuns}
|
||||
events={processEvents}
|
||||
selectedRunId={selectedRun?.run_id || null}
|
||||
onSelectRun={onSelectRun}
|
||||
onCancelRun={onCancelRun}
|
||||
/>
|
||||
</TabsContent>
|
||||
{hasResultsPanel && (
|
||||
<TabsContent value="results" className="flex-1 min-h-0 mt-0">
|
||||
<ArtifactSidebar
|
||||
selectedRun={selectedRun}
|
||||
events={processEvents}
|
||||
artifacts={processArtifacts}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,34 +5,34 @@ import remarkGfm from 'remark-gfm';
|
||||
|
||||
export function MarkdownContent({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||
<div className="prose prose-sm max-w-none text-[#1D1715] prose-headings:text-[#0B0B0B] prose-p:text-[#1D1715] prose-p:leading-7 prose-strong:text-[#0B0B0B] prose-a:text-[#342E2B] prose-a:underline prose-a:decoration-[#B8AEA8] prose-a:underline-offset-4 prose-li:text-[#1D1715] prose-blockquote:border-l-[#D8D2CE] prose-blockquote:text-[#4F4642] prose-code:rounded-md prose-code:bg-[#ECE8E5] prose-code:px-1.5 prose-code:py-0.5 prose-code:text-[#342E2B] prose-pre:border prose-pre:border-[#D8D2CE] prose-pre:bg-[#ECE8E5] prose-pre:text-[#342E2B] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
table: ({ children, ...props }) => (
|
||||
<div className="my-3 overflow-x-auto rounded-lg border border-border">
|
||||
<div className="my-3 overflow-x-auto rounded-lg border border-[#D8D2CE]">
|
||||
<table className="w-full border-collapse text-sm" {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children, ...props }) => (
|
||||
<thead className="bg-muted/60" {...props}>
|
||||
<thead className="bg-[#ECE8E5]" {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
th: ({ children, ...props }) => (
|
||||
<th className="px-3 py-2 text-left font-semibold text-foreground border-b border-border" {...props}>
|
||||
<th className="border-b border-[#D8D2CE] px-3 py-2 text-left font-semibold text-[#0B0B0B]" {...props}>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children, ...props }) => (
|
||||
<td className="px-3 py-2 border-b border-border/50" {...props}>
|
||||
<td className="border-b border-[#E7E2DE] px-3 py-2 text-[#1D1715]" {...props}>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
tr: ({ children, ...props }) => (
|
||||
<tr className="hover:bg-muted/30 transition-colors" {...props}>
|
||||
<tr className="transition-colors hover:bg-[#F7F5F4]" {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Bot, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Bot, CheckCircle2, ChevronRight, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react';
|
||||
|
||||
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
||||
import { getAccessToken, getFileUrl } from '@/lib/api';
|
||||
@ -44,24 +45,31 @@ function MessageBubble({
|
||||
}: {
|
||||
message: ChatMessage;
|
||||
canSendFeedback: boolean;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const isUser = message.role === 'user';
|
||||
const textContent = typeof message.content === 'string' ? message.content : String(message.content || '');
|
||||
const [feedbackMode, setFeedbackMode] = React.useState<'satisfied' | 'revise' | null>(null);
|
||||
const [feedbackComment, setFeedbackComment] = React.useState('');
|
||||
const validationFailed = message.validation_status === 'failed';
|
||||
const validationDetails =
|
||||
validationFailed
|
||||
? pickAppText(locale, '详细原因会在任务验证区展示;展开任务可查看验证报告。', 'Detailed reasons are shown in the task validation area. Open the task to inspect the validation report.')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
|
||||
{!isUser && (
|
||||
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<div className="mt-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-[#F1EFEE]">
|
||||
<Bot className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`rounded-xl px-4 py-3 max-w-[88%] shadow-sm ${
|
||||
className={`max-w-[88%] px-4 py-3 ${
|
||||
isUser
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-card border border-border/80'
|
||||
? 'rounded-[28px] bg-primary text-primary-foreground'
|
||||
: 'rounded-none bg-transparent text-[#1D1715]'
|
||||
}`}
|
||||
>
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
@ -110,42 +118,107 @@ function MessageBubble({
|
||||
) : (
|
||||
<MarkdownContent content={textContent} />
|
||||
)}
|
||||
{!isUser && message.task_id && (
|
||||
<div className="mt-3 rounded-md border border-border bg-muted/35 p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium uppercase text-muted-foreground">Task</div>
|
||||
<div className="mt-1 truncate text-sm font-medium">
|
||||
{pickAppText(locale, '已创建任务', 'Task created')}: {message.task_id}
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
{pickAppText(locale, '查看任务', 'Open task')}
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isUser && validationFailed && (
|
||||
<details className="mt-3 rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
||||
<summary className="cursor-pointer text-base font-semibold text-destructive">
|
||||
{pickAppText(locale, '验证失败', 'Validation failed')}
|
||||
</summary>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">{validationDetails}</p>
|
||||
</details>
|
||||
)}
|
||||
{!isUser && canSendFeedback && message.run_id && (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 border-t border-border/70 pt-2">
|
||||
<div className="mt-3 space-y-2 border-t border-border/70 pt-3">
|
||||
{message.feedback_state ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{message.feedback_state === 'satisfied'
|
||||
? pickAppText(locale, '已标记满意', 'Marked satisfied')
|
||||
: message.feedback_state === 'revise'
|
||||
? pickAppText(locale, '已请求修改', 'Revision requested')
|
||||
: pickAppText(locale, '已放弃任务', 'Task abandoned')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{message.feedback_state === 'satisfied'
|
||||
? pickAppText(locale, '已标记满意', 'Marked satisfied')
|
||||
: message.feedback_state === 'revise'
|
||||
? pickAppText(locale, '已请求修改', 'Revision requested')
|
||||
: pickAppText(locale, '已放弃任务', 'Task abandoned')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'satisfied')}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ThumbsUp className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '满意', 'Satisfied')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onFeedback(message.run_id!, 'revise')}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 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-7 items-center gap-1 rounded-md border border-border px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '放弃', 'Abandon')}
|
||||
</button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackMode('satisfied')}
|
||||
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"
|
||||
>
|
||||
<ThumbsUp className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '满意', 'Satisfied')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackMode('revise')}
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{pickAppText(locale, '放弃', 'Abandon')}
|
||||
</button>
|
||||
</div>
|
||||
{feedbackMode && (
|
||||
<div className="space-y-2 rounded-md border border-border bg-background p-2">
|
||||
<textarea
|
||||
value={feedbackComment}
|
||||
onChange={(event) => setFeedbackComment(event.target.value)}
|
||||
placeholder={
|
||||
feedbackMode === 'revise'
|
||||
? pickAppText(locale, '写下需要修改的地方...', 'Describe what needs to change...')
|
||||
: pickAppText(locale, '可选:补充说明...', 'Optional note...')
|
||||
}
|
||||
className="min-h-20 w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFeedbackMode(null);
|
||||
setFeedbackComment('');
|
||||
}}
|
||||
className="h-8 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"
|
||||
>
|
||||
{pickAppText(locale, '提交', 'Submit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{message.validation_status && message.validation_status !== 'unknown' && (
|
||||
@ -162,7 +235,7 @@ function MessageBubble({
|
||||
)}
|
||||
</div>
|
||||
{isUser && (
|
||||
<div className="w-7 h-7 rounded-full bg-secondary flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<div className="mt-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full bg-secondary">
|
||||
<User className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
@ -269,7 +342,7 @@ export function MessageList({
|
||||
selectedRunId: string | null;
|
||||
onSelectRun: (runId: string) => void;
|
||||
onCancelRun: (runId: string) => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon') => void;
|
||||
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
|
||||
}) {
|
||||
const { locale } = useAppI18n();
|
||||
const visibleMessages = React.useMemo(
|
||||
@ -311,12 +384,12 @@ export function MessageList({
|
||||
.find((message) => message.role === 'assistant' && message.run_id && message.task_id)?.run_id;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full px-4" viewportRef={viewportRef}>
|
||||
<div className="max-w-6xl mx-auto py-4 space-y-4">
|
||||
<ScrollArea className="h-full px-8" viewportRef={viewportRef}>
|
||||
<div className="mx-auto max-w-5xl space-y-8 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" />
|
||||
<p className="text-lg font-medium">Boardware Agent Sandbox</p>
|
||||
<p className="text-lg font-medium text-foreground">Beaver</p>
|
||||
<p className="text-sm">{pickAppText(locale, '发送消息开始对话', 'Send a message to start the conversation')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -13,11 +13,11 @@ import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function statusTone(status: string) {
|
||||
if (status === 'done') return 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20';
|
||||
if (status === 'error') return 'bg-rose-500/10 text-rose-300 border-rose-500/20';
|
||||
if (status === 'cancelled') return 'bg-zinc-500/10 text-zinc-300 border-zinc-500/20';
|
||||
if (status === 'waiting') return 'bg-amber-500/10 text-amber-300 border-amber-500/20';
|
||||
return 'bg-sky-500/10 text-sky-300 border-sky-500/20';
|
||||
if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]';
|
||||
if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]';
|
||||
if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]';
|
||||
if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]';
|
||||
return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]';
|
||||
}
|
||||
|
||||
function actorIcon(run: ProcessRun) {
|
||||
@ -147,7 +147,7 @@ export function ProcessLane({
|
||||
</div>
|
||||
))}
|
||||
{run.status === 'error' && (
|
||||
<div className="flex items-center gap-2 text-xs text-rose-300">
|
||||
<div className="flex items-center gap-2 text-xs text-[#5F5550]">
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
{pickAppText(locale, '此任务执行失败。', 'This task failed.')}
|
||||
</div>
|
||||
@ -168,8 +168,8 @@ function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
const rawEphemeral = metadata?.ephemeral_skill_names;
|
||||
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
|
||||
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
|
||||
const draftId = typeof metadata?.generated_skill_draft_id === 'string' ? metadata.generated_skill_draft_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !draftId) {
|
||||
const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : '';
|
||||
if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@ -184,9 +184,9 @@ function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
|
||||
ephemeral:{name}
|
||||
</Badge>
|
||||
))}
|
||||
{draftId && (
|
||||
{guidanceId && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
draft:{draftId.slice(0, 8)}
|
||||
guidance:{guidanceId.slice(0, 8)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -20,13 +20,13 @@ export function OfficeStatusBadge({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'border text-[11px]',
|
||||
status === 'done' && 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700',
|
||||
status === 'running' && 'border-sky-500/30 bg-sky-500/10 text-sky-700',
|
||||
status === 'waiting' && 'border-amber-500/30 bg-amber-500/10 text-amber-700',
|
||||
status === 'blocked' && 'border-orange-500/30 bg-orange-500/10 text-orange-700',
|
||||
status === 'queued' && 'border-slate-500/30 bg-slate-500/10 text-slate-700',
|
||||
status === 'error' && 'border-rose-500/30 bg-rose-500/10 text-rose-700',
|
||||
status === 'cancelled' && 'border-zinc-500/30 bg-zinc-500/10 text-zinc-700',
|
||||
status === 'done' && 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]',
|
||||
status === 'running' && 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]',
|
||||
status === 'waiting' && 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]',
|
||||
status === 'blocked' && 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]',
|
||||
status === 'queued' && 'border-[#D8D2CE] bg-[#ECE8E5] text-[#4F4642]',
|
||||
status === 'error' && 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]',
|
||||
status === 'cancelled' && 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@ -70,10 +70,10 @@ export function zonePanelClassName(zone: OfficeZoneView): string {
|
||||
return cn(
|
||||
'relative min-h-[220px] overflow-hidden rounded-2xl border p-4 shadow-sm',
|
||||
'before:pointer-events-none before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.9),transparent_40%)]',
|
||||
zone.tone === 'info' && 'border-sky-200 bg-[linear-gradient(180deg,rgba(240,249,255,0.95),rgba(224,242,254,0.7))]',
|
||||
zone.tone === 'warn' && 'border-amber-200 bg-[linear-gradient(180deg,rgba(255,251,235,0.95),rgba(254,243,199,0.72))]',
|
||||
zone.tone === 'danger' && 'border-rose-200 bg-[linear-gradient(180deg,rgba(255,241,242,0.96),rgba(255,228,230,0.76))]',
|
||||
zone.tone === 'success' && 'border-emerald-200 bg-[linear-gradient(180deg,rgba(236,253,245,0.96),rgba(209,250,229,0.74))]',
|
||||
zone.tone === 'info' && 'border-[#BCC4CE] bg-[#E4E7EB]/70',
|
||||
zone.tone === 'warn' && 'border-[#B8AEA8] bg-[#E7E2DE]/70',
|
||||
zone.tone === 'danger' && 'border-[#B8AEA8] bg-[#E7E2DE]/80',
|
||||
zone.tone === 'success' && 'border-[#B7C2B5] bg-[#E3E8E2]/75',
|
||||
zone.tone === 'neutral' && 'border-border bg-card'
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Building2, Clock3 } from 'lucide-react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Clock3, ListTodo } from 'lucide-react';
|
||||
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
@ -10,28 +10,30 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
const TASK_MANAGEMENT_TABS = [
|
||||
{
|
||||
label: 'Office',
|
||||
href: '/office',
|
||||
icon: Building2,
|
||||
match: (pathname: string) => pathname === '/office' || pathname.startsWith('/office/'),
|
||||
label: 'ordinary',
|
||||
href: '/tasks',
|
||||
icon: ListTodo,
|
||||
match: (pathname: string, tab: string | null) => pathname.startsWith('/tasks') && tab !== 'scheduled',
|
||||
},
|
||||
{
|
||||
label: 'Scheduled tasks',
|
||||
href: '/cron',
|
||||
label: 'scheduled',
|
||||
href: '/tasks?tab=scheduled',
|
||||
icon: Clock3,
|
||||
match: (pathname: string) => pathname === '/cron' || pathname.startsWith('/cron/'),
|
||||
match: (pathname: string, tab: string | null) => pathname.startsWith('/tasks') && tab === 'scheduled',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function TaskManagementTabs() {
|
||||
const { locale } = useAppI18n();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const activeTab = searchParams.get('tab');
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/70 bg-muted/20 p-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{TASK_MANAGEMENT_TABS.map((tab) => {
|
||||
const isActive = tab.match(pathname);
|
||||
const isActive = tab.match(pathname, activeTab);
|
||||
const Icon = tab.icon;
|
||||
|
||||
return (
|
||||
@ -46,9 +48,9 @@ export function TaskManagementTabs() {
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{tab.href === '/cron'
|
||||
{tab.label === 'scheduled'
|
||||
? pickAppText(locale, '定时任务', 'Scheduled tasks')
|
||||
: pickAppText(locale, '办公室', 'Office')}
|
||||
: pickAppText(locale, '普通任务', 'Ordinary tasks')}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium shadow-[0_1px_2px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.03)] ring-offset-background transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@ -13,7 +13,7 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
'border border-transparent bg-secondary text-secondary-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
|
||||
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
'rounded-2xl border border-black/[0.04] bg-card/70 text-card-foreground shadow-[0_1px_2px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.03)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background 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:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'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',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex min-h-[80px] w-full rounded-lg border border-transparent bg-input px-3 py-2 text-sm ring-offset-background 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}
|
||||
|
||||
Reference in New Issue
Block a user