update
This commit is contained in:
1 parent
b15f1c0e5e
commit
b56aaa1498
71 files changed
+866
-659
No files matched your search
@@ -46,3 +46,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
CLAUDE.md
|
||||
build/release/AgentCompany-0.4.61-arm64.dmg
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
@@ -121,3 +121,35 @@ Do not make direct repo edits outside a GSD workflow unless the user explicitly
|
||||
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
|
||||
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
||||
<!-- GSD:profile-end -->
|
||||
|
||||
## Design Context
|
||||
|
||||
### Users
|
||||
Builders and operators who want to spin up autonomous AI companies without managing infrastructure. They arrive with a goal, expect the system to self-organize, and watch their AI workforce execute. They care about output quality and want to feel like they're running something real — not playing with a demo.
|
||||
|
||||
### Brand Personality
|
||||
**Bold, futuristic, ambitious.** AgentCompany is the command layer for the next era of work. The interface should project quiet authority — this is a serious tool that happens to do something extraordinary.
|
||||
|
||||
### Emotional Goal
|
||||
**Professional pride.** The user should feel "I built something real." The interface should reinforce that their AI company is a legitimate operation — not a toy, not a gimmick, but a functioning organization they orchestrated.
|
||||
|
||||
### Aesthetic Direction
|
||||
- **Reference**: Notion — content-first layout, gentle hierarchy, flexible workspace feel
|
||||
- **Accent**: Steel slate (#4a5d7a light, #7b9cc4 dark) — formal, authoritative, professional
|
||||
- **Theme**: Full light + dark mode via CSS custom properties
|
||||
- **Typography**: System sans-serif for UI, JetBrains Mono for code/data
|
||||
- **Icons**: Phosphor (regular + fill weights), 16-18px standard
|
||||
- **Motion**: Framer Motion with cubic-bezier(0.16, 1, 0.3, 1), reduced-motion aware
|
||||
|
||||
### Anti-References (what this must NOT be)
|
||||
- **Not toy/demo**: No cartoon illustrations, rainbow gradients, or playful AI mascots
|
||||
- **Not enterprise bloat**: No overwhelming dashboards, feature overload, or Salesforce-style complexity
|
||||
- **Not generic SaaS**: No cookie-cutter component libraries, stock photo energy, or bland templates
|
||||
- **Not sci-fi gimmick**: No glowing neon, Matrix terminals, or over-the-top futuristic theming
|
||||
|
||||
### Design Principles
|
||||
1. **Content over chrome** — Information density is a feature. Every element earns its place. No decorative filler.
|
||||
2. **Quiet authority** — The interface should feel confident without shouting. Subtle borders, restrained color, generous whitespace.
|
||||
3. **Real, not theatrical** — Agent activity should feel like watching a real company operate, not a sci-fi simulation. Status over spectacle.
|
||||
4. **Desktop-native craft** — Keyboard-first, focus rings, platform-aware shortcuts, proper window management. No web-app-in-a-wrapper feel.
|
||||
5. **Accessible by default** — Reduced motion, color contrast, semantic HTML, focus traps. Not an afterthought.
|
||||
@@ -4,7 +4,7 @@
|
||||
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
|
||||
},
|
||||
"extraMetadata": {
|
||||
"version": "0.4.61"
|
||||
"version": "0.4.77"
|
||||
},
|
||||
"mac": {
|
||||
"hardenedRuntime": false,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
version: 0.4.61
|
||||
version: 0.4.77
|
||||
files:
|
||||
- url: AgentCompany-0.4.61-arm64-mac.zip
|
||||
sha512: 9qHqwpYcc4lCuAfqazR5duwAIYvMoMWufWtw+Wm1gyVGfwyb58szRmzgWHBErYHx/51eQHZL7i6RhJswmIvIrg==
|
||||
size: 113638909
|
||||
- url: AgentCompany-0.4.61-arm64.dmg
|
||||
sha512: tuE2rcS0lPGMvB0lOnxX3udIl+oESrZWqRrYXuzIV7gsBfp8NL26fLxjppt6BhQbVT6GeKHi7p7OdViZKxYL2Q==
|
||||
size: 118393537
|
||||
path: AgentCompany-0.4.61-arm64-mac.zip
|
||||
sha512: 9qHqwpYcc4lCuAfqazR5duwAIYvMoMWufWtw+Wm1gyVGfwyb58szRmzgWHBErYHx/51eQHZL7i6RhJswmIvIrg==
|
||||
releaseDate: '2026-03-23T16:31:23.663Z'
|
||||
- url: AgentCompany-0.4.77-arm64-mac.zip
|
||||
sha512: rM2oPamJ0mHhH4bkIrcdJT+raflsJpiKI331HDD/flZvGnB0L3zveHEjkG6eZkIvpQOh78d9/HjPRUuB2lGZ+w==
|
||||
size: 113614712
|
||||
path: AgentCompany-0.4.77-arm64-mac.zip
|
||||
sha512: rM2oPamJ0mHhH4bkIrcdJT+raflsJpiKI331HDD/flZvGnB0L3zveHEjkG6eZkIvpQOh78d9/HjPRUuB2lGZ+w==
|
||||
releaseDate: '2026-03-23T20:26:33.820Z'
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-company",
|
||||
"version": "0.4.61",
|
||||
"version": "0.4.77",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
||||
|
||||
+63
-58
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
ArrowClockwise,
|
||||
CaretUpDown,
|
||||
ChatCircle,
|
||||
Check,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
MagnifyingGlass,
|
||||
Megaphone,
|
||||
Plus,
|
||||
Robot,
|
||||
UsersThree,
|
||||
ShieldCheck,
|
||||
Trash,
|
||||
UserPlus,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
Target,
|
||||
SidebarSimple,
|
||||
TerminalWindow,
|
||||
CircleNotch,
|
||||
WarningCircle,
|
||||
X,
|
||||
} from "@phosphor-icons/react";
|
||||
@@ -77,6 +77,7 @@ import { CommunicationHub } from "./components/communication";
|
||||
import { HiringPipeline } from "./components/hiring";
|
||||
import { DocumentCenter, KnowledgeBaseManager, MeetingsManager, SprintManager } from "./components/company-operations";
|
||||
import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionButton, Select, StatusPill, useFocusTrap, useReducedMotion } from "./components/ui";
|
||||
import ToastContainer from "./components/Toast";
|
||||
import { unwrap } from "./lib/desktop";
|
||||
import { formatMoney, formatTime } from "./lib/formatters";
|
||||
|
||||
@@ -90,7 +91,7 @@ type PaletteAction = {
|
||||
|
||||
const sidebarNavDefs: NavItemDef[] = [
|
||||
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
|
||||
{ id: "agents", labelKey: "nav.agents", icon: Robot },
|
||||
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
|
||||
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
|
||||
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
||||
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
||||
@@ -101,8 +102,8 @@ const sidebarNavDefs: NavItemDef[] = [
|
||||
export function App() {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const noMotion = { duration: 0 };
|
||||
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const pageTransition = reducedMotion ? noMotion : { duration: 0.32, ease: [0.16, 1, 0.3, 1] as const };
|
||||
const panelTransition = reducedMotion ? noMotion : { duration: 0.28, ease: [0.16, 1, 0.3, 1] as const };
|
||||
|
||||
// -- Zustand store bindings (replaces 31 useState calls) --
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
@@ -556,24 +557,11 @@ export function App() {
|
||||
setInitialOnboardingLocked(false);
|
||||
await refresh();
|
||||
setError(null);
|
||||
setSection("overview");
|
||||
if (summary.runId) {
|
||||
setSection("runs");
|
||||
setSelectedRunId(summary.runId);
|
||||
setConsoleOpen(true);
|
||||
return;
|
||||
}
|
||||
if (summary.taskId) {
|
||||
setSection("tasks");
|
||||
setSelectedTaskId(summary.taskId);
|
||||
return;
|
||||
}
|
||||
if (summary.projectId) {
|
||||
setSection("projects");
|
||||
setSelectedProjectId(summary.projectId);
|
||||
return;
|
||||
}
|
||||
if (summary.leadAgentId) {
|
||||
setSection("agents");
|
||||
setSelectedAgentId(summary.leadAgentId);
|
||||
}
|
||||
}, [refresh]);
|
||||
@@ -582,8 +570,9 @@ export function App() {
|
||||
return (
|
||||
<LocaleContext.Provider value={locale}>
|
||||
<TranslationContext.Provider value={t}>
|
||||
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
|
||||
{t("app.loading")}
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-3">
|
||||
<CircleNotch size={24} className="animate-spin text-[color:var(--accent)]" />
|
||||
<span className="text-[13px] text-[color:var(--muted)]">{t("app.loading")}</span>
|
||||
</div>
|
||||
</TranslationContext.Provider>
|
||||
</LocaleContext.Provider>
|
||||
@@ -616,7 +605,7 @@ export function App() {
|
||||
<LocaleContext.Provider value={locale}>
|
||||
<TranslationContext.Provider value={t}>
|
||||
<div className={`grid h-[100dvh] overflow-hidden bg-[color:var(--bg)] ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
|
||||
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]">
|
||||
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line-strong)] bg-[color:var(--panel)]">
|
||||
{sidebarCollapsed ? (
|
||||
<div className="sidebar-drag flex items-center justify-center px-2 pb-3 pt-[38px]">
|
||||
<button
|
||||
@@ -651,7 +640,7 @@ export function App() {
|
||||
<span className="flex-1 truncate text-[12px] text-[color:var(--danger)]">Delete {company.name}?</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)] hover:text-white"
|
||||
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"
|
||||
onClick={() => void deleteCompany(company.id)}
|
||||
>
|
||||
Yes
|
||||
@@ -678,7 +667,7 @@ export function App() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-2 rounded p-1 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger)] hover:text-white group-hover:opacity-100"
|
||||
className="mr-2 rounded p-1 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)] group-hover:opacity-100"
|
||||
title={`Delete ${company.name}`}
|
||||
onClick={() => setConfirmDeleteCompanyId(company.id)}
|
||||
>
|
||||
@@ -749,56 +738,51 @@ export function App() {
|
||||
</aside>
|
||||
|
||||
<main aria-label="Content" className="flex min-w-0 flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="mx-auto flex w-full max-w-[1040px] flex-col px-6 py-6 lg:px-10 lg:py-8">
|
||||
<header className="mb-8 flex items-start justify-between gap-4 border-b border-[color:var(--line)] pb-5 lg:mb-10 lg:gap-6">
|
||||
<h1 className="text-[24px] tracking-[-0.03em] text-[color:var(--text)] lg:text-[31px]">
|
||||
{navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
|
||||
</h1>
|
||||
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
onClick={() => void refresh()}
|
||||
>
|
||||
<ArrowClockwise size={16} />
|
||||
<span className="hidden lg:inline">{t("app.refresh")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={consoleOpen ? "Hide console" : "Show console"}
|
||||
title={consoleOpen ? "Hide console" : "Show console"}
|
||||
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
onClick={() => setConsoleOpen(!consoleOpen)}
|
||||
>
|
||||
<TerminalWindow size={16} />
|
||||
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
|
||||
</button>
|
||||
<header className="sidebar-drag shrink-0 border-b border-[color:var(--line)] bg-[color:var(--bg)]">
|
||||
<div className="mx-auto flex w-full max-w-[1040px] items-start justify-between gap-4 px-6 pb-5 pt-6 lg:gap-6 lg:px-10 lg:pt-8">
|
||||
<h1 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">
|
||||
{navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
|
||||
</h1>
|
||||
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={consoleOpen ? "Hide console" : "Show console"}
|
||||
title={consoleOpen ? "Hide console" : "Show console"}
|
||||
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
onClick={() => setConsoleOpen(!consoleOpen)}
|
||||
>
|
||||
<TerminalWindow size={16} />
|
||||
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className={`flex-1 ${section === "communication" ? "flex flex-col overflow-hidden" : "overflow-auto"}`}>
|
||||
<div className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "mx-auto flex w-full max-w-[1040px] flex-col px-6 py-6 lg:px-10 lg:py-8"}>
|
||||
|
||||
{error ? (
|
||||
<div className="mb-6 flex items-center gap-3 rounded-[10px] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className={`flex items-center gap-3 rounded-[8px] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)] ${section === "communication" ? "mx-6 mt-4" : "mb-6"}`}>
|
||||
<WarningCircle size={18} />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<div className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "min-h-0 flex-1"}>
|
||||
<ErrorBoundary>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={section}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={pageTransition}
|
||||
className="space-y-14"
|
||||
className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "space-y-14"}
|
||||
style={section === "communication" ? { height: "100%" } : undefined}
|
||||
>
|
||||
{section === "overview" ? (
|
||||
<Dashboard
|
||||
companyName={currentCompany?.name}
|
||||
agents={companyAgents}
|
||||
tasks={tasks}
|
||||
runs={runs}
|
||||
@@ -992,6 +976,26 @@ export function App() {
|
||||
|
||||
{section === "tasks" ? (
|
||||
<>
|
||||
<Panel title="Goals" action={<ActionButton label="New goal" icon={Target} tone="accent" onClick={() => setSelectedGoalId("__new__")} />}>
|
||||
<GoalList goals={companyGoals} agents={companyAgents} selectedGoalId={selectedGoalId} onSelect={setSelectedGoalId} />
|
||||
</Panel>
|
||||
<Modal open={selectedGoalId === "__new__" && section === "tasks"} title="New goal" onClose={() => setSelectedGoalId(null)}>
|
||||
<GoalCreator companyId={currentCompany!.id} agents={companyAgents} goals={companyGoals} onSaved={async () => { setSelectedGoalId(null); await refresh(); }} />
|
||||
</Modal>
|
||||
<Modal open={!!selectedGoal && section === "tasks"} title={selectedGoal ? `Edit: ${selectedGoal.title}` : ""} onClose={() => setSelectedGoalId(null)}>
|
||||
{selectedGoal ? (
|
||||
<GoalEditor
|
||||
key={selectedGoalId}
|
||||
goal={selectedGoal}
|
||||
goals={companyGoals}
|
||||
agents={companyAgents}
|
||||
companyId={currentCompany!.id}
|
||||
onSaved={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
onDeleted={async () => { setSelectedGoalId(null); await refresh(); }}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Panel
|
||||
title="Task queue"
|
||||
action={
|
||||
@@ -1387,7 +1391,7 @@ export function App() {
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.98 }}
|
||||
transition={panelTransition}
|
||||
className="w-full max-w-[560px] rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
|
||||
className="w-full max-w-[560px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
@@ -1458,7 +1462,7 @@ export function App() {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-[color:var(--text)]">
|
||||
<Robot size={16} />
|
||||
<UsersThree size={16} />
|
||||
<span className="text-[13px] tracking-[0.005em]">{agent.name}</span>
|
||||
<span className="text-[11px] text-[color:var(--muted)]">{agent.role}</span>
|
||||
</div>
|
||||
@@ -1594,6 +1598,7 @@ export function App() {
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</TranslationContext.Provider>
|
||||
</LocaleContext.Provider>
|
||||
|
||||
@@ -28,14 +28,14 @@ export function ClaudeSettingsDialog({
|
||||
}: ClaudeSettingsDialogProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--bg)]/72 backdrop-blur-sm" onClick={onClose}>
|
||||
<div className="w-full max-w-[420px] rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-[16px] font-semibold text-[color:var(--text)]">Claude Code {currentLocale === "zh" ? "设置" : "Settings"}</h3>
|
||||
<button type="button" onClick={onClose} className="rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={16} /></button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">API Key</label>
|
||||
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
|
||||
@@ -46,7 +46,7 @@ export function ClaudeSettingsDialog({
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Base URL</label>
|
||||
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Base URL</label>
|
||||
<input
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
|
||||
value={claudeBaseUrl}
|
||||
@@ -56,7 +56,7 @@ export function ClaudeSettingsDialog({
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Model</label>
|
||||
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Model</label>
|
||||
<input
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
|
||||
value={claudeModel}
|
||||
@@ -74,7 +74,7 @@ export function ClaudeSettingsDialog({
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
className="rounded-[8px] bg-[color:var(--accent)] px-4 py-2 text-[13px] font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||
className="rounded-[8px] bg-[color:var(--accent)] px-4 py-2 text-[13px] font-medium text-[color:var(--text-on-accent)] transition hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? (currentLocale === "zh" ? "保存中..." : "Saving...") : (currentLocale === "zh" ? "保存" : "Save")}
|
||||
</button>
|
||||
|
||||
@@ -54,7 +54,7 @@ export function CompanyStep({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -18 }}
|
||||
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="surface w-full rounded-[12px] p-7"
|
||||
className="surface w-full rounded-[8px] p-7"
|
||||
>
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{t("onboarding.stepLabel1")} 1 / 3
|
||||
@@ -87,7 +87,7 @@ export function CompanyStep({
|
||||
label: connector.label,
|
||||
}))}
|
||||
/>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
|
||||
{t("onboarding.connectorInfo")}
|
||||
</div>
|
||||
<div>
|
||||
@@ -130,7 +130,7 @@ export function CompanyStep({
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ConnectorReadiness({
|
||||
})}
|
||||
</div>
|
||||
{!hasRunnableConnector ? (
|
||||
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
|
||||
<div className="mt-4 rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
|
||||
{t("onboarding.connectorWarning")}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function LanguageStep({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -18 }}
|
||||
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="surface w-full rounded-[12px] p-7"
|
||||
className="surface w-full rounded-[8px] p-7"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<GlobeSimple size={12} />
|
||||
@@ -41,7 +41,7 @@ export function LanguageStep({
|
||||
key={locale}
|
||||
type="button"
|
||||
onClick={() => onLocaleChange(locale)}
|
||||
className={`flex w-full items-center gap-3 rounded-[10px] border px-4 py-3.5 text-left transition ${
|
||||
className={`flex w-full items-center gap-3 rounded-[8px] border px-4 py-3.5 text-left transition ${
|
||||
currentLocale === locale
|
||||
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
|
||||
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
|
||||
@@ -52,7 +52,7 @@ export function LanguageStep({
|
||||
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
|
||||
: "border-[color:var(--line)]"
|
||||
}`}>
|
||||
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-white" /> : null}
|
||||
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-[color:var(--text-on-accent)]" /> : null}
|
||||
</div>
|
||||
<span className={`text-[14px] font-medium ${
|
||||
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
|
||||
|
||||
@@ -26,7 +26,7 @@ export function LaunchStep({
|
||||
initial={{ opacity: 0, scale: 0.96 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="surface w-full rounded-[12px] p-7"
|
||||
className="surface w-full rounded-[8px] p-7"
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[color:var(--success-soft)] text-[color:var(--success)]">
|
||||
<CheckCircle size={24} weight="fill" />
|
||||
@@ -39,31 +39,31 @@ export function LaunchStep({
|
||||
</p>
|
||||
|
||||
<div className="mt-6 grid gap-3 md:grid-cols-2">
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
|
||||
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||
{selectedConnector?.label ?? t("onboarding.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
|
||||
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
|
||||
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
|
||||
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
|
||||
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
|
||||
@@ -72,7 +72,7 @@ export function LaunchStep({
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function MissionStep({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -18 }}
|
||||
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="surface w-full rounded-[12px] p-7"
|
||||
className="surface w-full rounded-[8px] p-7"
|
||||
>
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{t("onboarding.stepLabel2")} 2 / 3
|
||||
@@ -70,7 +70,7 @@ export function MissionStep({
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-5 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -41,34 +41,83 @@ export function OnboardingHero({
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 items-center justify-center overflow-y-auto px-8 py-12 md:px-16">
|
||||
<div className="relative flex min-h-0 items-center justify-center overflow-y-auto px-8 py-12 md:px-16">
|
||||
{/* Ambient gradient - subtle animated accent wash */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.035]"
|
||||
style={{
|
||||
background: "radial-gradient(ellipse 80% 60% at 30% 20%, var(--accent) 0%, transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.025]"
|
||||
style={{
|
||||
background: "radial-gradient(ellipse 60% 80% at 70% 80%, var(--accent) 0%, transparent 70%)",
|
||||
animation: "hero-drift 12s ease-in-out infinite alternate",
|
||||
}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mx-auto w-full max-w-[460px]"
|
||||
className="relative z-10 mx-auto w-full max-w-[460px]"
|
||||
>
|
||||
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{t("onboarding.subtitle")}
|
||||
</div>
|
||||
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
|
||||
{/* Signature line */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mb-6 flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
className="h-px flex-1 max-w-[32px]"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, var(--accent), transparent)",
|
||||
}}
|
||||
/>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[color:var(--muted)]">
|
||||
{t("onboarding.subtitle")}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.15, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-[36px] font-bold leading-[1.05] tracking-[-0.03em] text-[color:var(--text)] md:text-[44px]"
|
||||
>
|
||||
{t("onboarding.title").split("\n").map((line, i) => (
|
||||
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
|
||||
))}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
|
||||
{t("onboarding.description")}
|
||||
</p>
|
||||
</motion.h1>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.25, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]"
|
||||
>
|
||||
{t("onboarding.description")}
|
||||
</motion.p>
|
||||
|
||||
{/* Step progress */}
|
||||
{step !== "language" ? (
|
||||
<div className="mt-10 flex items-center gap-3">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.35, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mt-10 flex items-center gap-3"
|
||||
>
|
||||
{stepLabels.map((entry, index) => (
|
||||
<div key={entry.label} className="flex items-center gap-2">
|
||||
{index > 0 ? (
|
||||
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
|
||||
<div className={`h-px w-6 transition-colors duration-300 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
|
||||
) : null}
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
|
||||
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium transition-colors duration-300 ${
|
||||
index < stepIndex
|
||||
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
|
||||
: index === stepIndex
|
||||
@@ -81,9 +130,10 @@ export function OnboardingHero({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{/* Connector readiness */}
|
||||
<ConnectorReadiness
|
||||
connectors={connectors}
|
||||
currentLocale={currentLocale}
|
||||
@@ -91,7 +141,41 @@ export function OnboardingHero({
|
||||
onOpenClaudeSettings={onOpenClaudeSettings}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* ELYDORA signature */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mt-12 flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
className="h-px w-8"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, var(--accent), transparent)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="select-none text-[10px] font-semibold uppercase tracking-[0.2em]"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, var(--accent) 0%, var(--muted) 100%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
backgroundClip: "text",
|
||||
}}
|
||||
>
|
||||
by ELYDORA
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Inline keyframe for ambient drift */}
|
||||
<style>{`
|
||||
@keyframes hero-drift {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
100% { transform: translate(-4%, 3%) scale(1.05); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -184,7 +184,9 @@ export function CompanyOnboarding({
|
||||
}, [launchSummary, onDone, t]);
|
||||
|
||||
return (
|
||||
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
|
||||
<div className="relative grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
|
||||
{/* Drag region for macOS frameless title bar */}
|
||||
<div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" />
|
||||
<OnboardingHero
|
||||
step={step}
|
||||
connectors={connectors}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { CheckCircle, Warning, X, XCircle } from "@phosphor-icons/react"
|
||||
import { useAppStore } from "../state/store"
|
||||
import { useReducedMotion } from "./ui"
|
||||
|
||||
const TONE_CONFIG = {
|
||||
success: { icon: CheckCircle, bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" },
|
||||
danger: { icon: XCircle, bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" },
|
||||
warn: { icon: Warning, bg: "var(--warn-soft)", border: "var(--warn)", text: "var(--warn)" },
|
||||
} as const
|
||||
|
||||
export default function ToastContainer() {
|
||||
const toasts = useAppStore((s) => s.toasts)
|
||||
const dismiss = useAppStore((s) => s.dismissToast)
|
||||
const reduced = useReducedMotion()
|
||||
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[60] flex flex-col gap-2" aria-live="polite">
|
||||
<AnimatePresence>
|
||||
{toasts.map((toast) => {
|
||||
const cfg = TONE_CONFIG[toast.tone]
|
||||
const Icon = cfg.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={toast.id}
|
||||
initial={reduced ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.96 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.96 }}
|
||||
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex w-[320px] items-start gap-2.5 rounded-[8px] border px-4 py-3 shadow-lg"
|
||||
style={{ backgroundColor: cfg.bg, borderColor: cfg.border }}
|
||||
>
|
||||
<Icon size={16} weight="fill" className="mt-px shrink-0" style={{ color: cfg.text }} />
|
||||
<span className="min-w-0 flex-1 break-words text-[13px] font-medium" style={{ color: cfg.text }}>
|
||||
{toast.message}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dismiss(toast.id)}
|
||||
className="shrink-0 rounded-[4px] p-0.5 opacity-60 transition hover:opacity-100"
|
||||
style={{ color: cfg.text }}
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function DeleteConfirmDialog({ open, entityName, onConfirm, onCancel }: { open:
|
||||
if (!open) return null
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onCancel}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-[14px] border bg-[color:var(--panel)] p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-[8px] border bg-[color:var(--panel)] p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-[color:var(--danger-soft)]"><Trash size={16} className="text-[color:var(--danger)]" /></div><h3 className="text-[15px] font-bold text-[color:var(--text)]">Delete</h3></div>
|
||||
<p className="mt-2 text-[13px] leading-relaxed text-[color:var(--muted)]">Are you sure you want to delete <strong className="text-[color:var(--text)]">{entityName}</strong>? This action cannot be undone.</p>
|
||||
<div className="mt-5 flex justify-end gap-3"><button type="button" onClick={onCancel} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">Cancel</button><ActionButton label="Delete" onClick={onConfirm} icon={Trash} tone="danger" /></div>
|
||||
@@ -47,13 +47,13 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-white"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Automation Rules</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--success)" }].map((s) => <div key={s.l} className="rounded-[10px] border bg-[color:var(--panel)] px-4 py-3" style={{ borderColor: LINE }}><div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.l}</div><div className="text-[22px] font-bold" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
|
||||
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Automation Rules</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--success)" }].map((s) => <div key={s.l} className="rounded-[8px] border bg-[color:var(--panel)] px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
|
||||
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Activity Log ({actLog.length})</button></div>
|
||||
{activeTab === "rules" ? <>
|
||||
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-white" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
|
||||
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
|
||||
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
|
||||
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
|
||||
: filtered.length === 0 ? <div className="py-8 text-center text-[13px] text-[color:var(--muted)]">No rules match the current filter.</div>
|
||||
: <div className="space-y-2"><AnimatePresence>{filtered.map((r) => <RuleCard key={r.id} rule={r} onToggle={() => void handleToggle(r)} onDelete={() => setDelTarget(r)} onEdit={() => setEditRule(r)} />)}</AnimatePresence></div>}
|
||||
</> : <AutomationActivityLog entries={actLog} />}
|
||||
@@ -71,9 +71,9 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
|
||||
const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-white"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Workflow Pipelines</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
|
||||
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Workflow Pipelines</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
|
||||
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
|
||||
: <div className="space-y-2"><AnimatePresence>{workflows.map((wf) => <WorkflowCard key={wf.id} workflow={wf} isRunning={runningId === wf.id} onDelete={() => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}</AnimatePresence></div>}
|
||||
<AnimatePresence>{showCreate ? <WorkflowFormModal open={showCreate} editingWorkflow={null} companyId={companyId} onClose={() => setShowCreate(false)} onSaved={onRefresh} /> : null}{editWf ? <WorkflowFormModal open={!!editWf} editingWorkflow={editWf} companyId={companyId} onClose={() => setEditWf(null)} onSaved={onRefresh} /> : null}</AnimatePresence>
|
||||
<AnimatePresence>{delTarget ? <DeleteConfirmDialog open={!!delTarget} entityName={delTarget.name} onConfirm={() => void handleDelete()} onCancel={() => setDelTarget(null)} /> : null}</AnimatePresence>
|
||||
@@ -85,8 +85,8 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
|
||||
const t = useT(); const [activeSection, setActiveSection] = useState<"rules"|"workflows">("rules")
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-[12px] border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
|
||||
<div className="flex gap-1 rounded-[10px] bg-[color:var(--panel-soft)] p-1">
|
||||
<div className="rounded-[8px] border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
|
||||
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1">
|
||||
<button type="button" onClick={() => setActiveSection("rules")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} style={activeSection === "rules" ? { color: ACCENT } : undefined} />Automation Rules ({rules.length})</button>
|
||||
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} style={activeSection === "workflows" ? { color: ACCENT } : undefined} />Workflows ({workflows.length})</button>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,13 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
|
||||
const TriggerIcon = triggerInfo?.icon ?? Lightning; const colors = statusColors[rule.status]
|
||||
|
||||
return (
|
||||
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[10px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
|
||||
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition duration-150 hover:shadow-md hover:-translate-y-px" style={{ borderColor: LINE }}>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
|
||||
<button type="button" onClick={onEdit} className="min-w-0 flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-[13px] font-semibold text-[color:var(--text)]">{rule.name}</span>
|
||||
<span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{rule.status}</span>
|
||||
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{rule.name}</span>
|
||||
<span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} aria-hidden="true" />{rule.status}</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[color:var(--muted)]">
|
||||
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? rule.trigger}</span>
|
||||
@@ -23,7 +23,7 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
|
||||
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Gear size={10} />{actionInfo?.label ?? rule.action}</span>
|
||||
{rule.sourceDepartment || rule.targetDepartment ? <><span className="text-[color:var(--line-strong)]">|</span><span className="inline-flex items-center gap-1"><span className="font-medium text-[color:var(--muted-strong)]">{rule.sourceDepartment ?? "Any"}</span><CaretRight size={8} /><span className="font-medium text-[color:var(--muted-strong)]">{rule.targetDepartment ?? "Same"}</span></span></> : null}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-[10px] text-[color:var(--muted)]"><span>{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Priority {rule.priority}</span></div>
|
||||
<div className="mt-1 flex items-center gap-3 text-[10px] text-[color:var(--muted)]"><span className="tabular-nums">{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span className="tabular-nums">Priority {rule.priority}</span></div>
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button type="button" onClick={onToggle} aria-label={rule.status === "active" ? "Pause rule" : "Activate rule"} title={rule.status === "active" ? "Pause rule" : "Activate rule"} className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]">{rule.status === "active" ? <Pause size={15} /> : <Play size={15} />}</button>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved }
|
||||
const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action]
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-[14px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}>
|
||||
<div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Automation Rule" : "New Automation Rule"}</h2></div>
|
||||
<button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function WorkflowFormModal({ open, editingWorkflow, companyId, onClose, o
|
||||
const statusOptions = [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "paused", label: "Paused" }]
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-[14px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}><div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><ArrowsClockwise size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Workflow" : "New Workflow"}</h2></div><button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button></div>
|
||||
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
|
||||
{error ? <div className="rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]">{error}</div> : null}
|
||||
|
||||
@@ -35,9 +35,9 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
|
||||
const steps: WorkflowStep[] = useMemo(() => { const parsed = safeJsonParse(workflow.stepsJson); return Array.isArray(parsed) ? (parsed as WorkflowStep[]) : [] }, [workflow.stepsJson])
|
||||
|
||||
return (
|
||||
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[10px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
|
||||
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
|
||||
<button type="button" onClick={() => setExpanded((p) => !p)} className="min-w-0 flex-1 text-left">
|
||||
<div className="flex items-center gap-2"><span className="truncate text-[13px] font-semibold text-[color:var(--text)]">{workflow.name}</span><span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{workflow.status}</span></div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[11px] text-[color:var(--muted)]"><span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? workflow.triggerType}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{steps.length} step{steps.length !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{workflow.runCount} run{workflow.runCount !== 1 ? "s" : ""}</span>{workflow.lastRunAt ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(workflow.lastRunAt)}</span></> : null}</div>
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import type { AgentMessageRecord, CommentRecord, TaskRecord, MessageChannel } from "@shared/types"
|
||||
import { ChatCircle, Hash, CaretDown } from "@phosphor-icons/react"
|
||||
import { CaretDown } from "@phosphor-icons/react"
|
||||
import { useT } from "../../i18n"
|
||||
|
||||
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
|
||||
direct: { label: "Direct Messages", icon: ChatCircle, color: "var(--accent)" },
|
||||
department: { label: "Department", icon: ChatCircle, color: "#6366f1" },
|
||||
company: { label: "Company-wide", icon: ChatCircle, color: "#d97745" },
|
||||
project: { label: "Project", icon: Hash, color: "#10b981" },
|
||||
incident: { label: "Incident", icon: ChatCircle, color: "var(--danger)" },
|
||||
}
|
||||
|
||||
const TERRACOTTA = "#d97745"
|
||||
|
||||
export interface ChannelDescriptor {
|
||||
key: string
|
||||
kind: "message"
|
||||
@@ -70,51 +60,19 @@ export function ChannelHeader({
|
||||
) : null}
|
||||
|
||||
{activeChannel ? (
|
||||
<>
|
||||
{activeChannel.kind === "message" ? (() => {
|
||||
const Icon = channelConfig[activeChannel.channel].icon
|
||||
return (
|
||||
<Icon
|
||||
size={16}
|
||||
weight="fill"
|
||||
style={{
|
||||
color: channelConfig[activeChannel.channel].color,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})() : (
|
||||
<Hash size={16} weight="fill" style={{ color: TERRACOTTA }} />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] font-semibold text-[color:var(--text)]">
|
||||
{activeChannel.label}
|
||||
</div>
|
||||
<div className="text-[10px] text-[color:var(--muted)]">
|
||||
{activeChannel.kind === "message"
|
||||
? `${channelMessages.length} message${channelMessages.length !== 1 ? "s" : ""}${(() => {
|
||||
const stats = unreadByChannel.get(activeChannel.key)
|
||||
return stats?.unread ? ` (${stats.unread} unread)` : ""
|
||||
})()}`
|
||||
: `${taskDiscussionComments.length} comment${taskDiscussionComments.length !== 1 ? "s" : ""}${activeTask ? ` · ${activeTask.status.replaceAll("_", " ")}` : ""}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-semibold text-[color:var(--text)]">{activeChannel.label}</div>
|
||||
{activeChannel.kind === "task" && activeTask ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigateToTask(activeTask.id)}
|
||||
className="rounded-[6px] border border-[color:var(--line)] px-2.5 py-1 text-[11px] font-medium text-[color:var(--muted-strong)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
|
||||
>
|
||||
<button type="button" onClick={() => onNavigateToTask(activeTask.id)} className="mt-0.5 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
||||
Open task
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[13px] text-[color:var(--muted)]">
|
||||
{t("comm.selectChannel")}
|
||||
</span>
|
||||
<span className="text-[13px] text-[color:var(--muted)]">{t("comm.selectChannel")}</span>
|
||||
)}
|
||||
|
||||
<div className="ml-auto w-[320px]">
|
||||
<div className="ml-auto w-full max-w-[320px]">
|
||||
{filterBar}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { useState } from "react"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { EnvelopeSimple, Hash, Buildings, Megaphone, Fire, CaretDown, MagnifyingGlass, Funnel } from "@phosphor-icons/react"
|
||||
import { EnvelopeSimple, Hash, Buildings, Megaphone, Fire, MagnifyingGlass, Funnel } from "@phosphor-icons/react"
|
||||
import type { AgentRecord, MessageChannel } from "@shared/types"
|
||||
import { AgentAvatar } from "./MessageItem"
|
||||
import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeader"
|
||||
import { useT } from "../../i18n"
|
||||
|
||||
const TERRACOTTA = "#d97745"
|
||||
|
||||
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
|
||||
direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--accent)" },
|
||||
department: { label: "Department", icon: Buildings, color: "#6366f1" },
|
||||
company: { label: "Company-wide", icon: Megaphone, color: TERRACOTTA },
|
||||
project: { label: "Project", icon: Hash, color: "#10b981" },
|
||||
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
|
||||
company: { label: "Company-wide", icon: Megaphone, color: "var(--accent)" },
|
||||
project: { label: "Project", icon: Hash, color: "var(--success)" },
|
||||
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
|
||||
}
|
||||
|
||||
@@ -40,8 +38,8 @@ function ChannelItem({
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
|
||||
active ? "bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
|
||||
className={`group relative flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
|
||||
active ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
|
||||
}`}
|
||||
>
|
||||
{descriptor.channel === "direct" && agent ? (
|
||||
@@ -56,7 +54,7 @@ function ChannelItem({
|
||||
)}
|
||||
|
||||
<span
|
||||
className={`min-w-0 flex-1 truncate text-[12px] ${
|
||||
className={`min-w-0 flex-1 truncate text-[13px] ${
|
||||
active
|
||||
? "font-semibold text-[color:var(--text)]"
|
||||
: unreadCount > 0
|
||||
@@ -71,8 +69,8 @@ function ChannelItem({
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-[color:var(--danger)]" />
|
||||
) : unreadCount > 0 ? (
|
||||
<span
|
||||
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-white"
|
||||
style={{ backgroundColor: TERRACOTTA }}
|
||||
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-[color:var(--text-on-accent)]"
|
||||
style={{ backgroundColor: "var(--accent)" }}
|
||||
>
|
||||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
@@ -113,24 +111,10 @@ export function ChannelSidebar({
|
||||
className="flex h-full shrink-0 flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]"
|
||||
style={{ width: 260 }}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
|
||||
<span className="text-[13px] font-semibold text-[color:var(--text)]">
|
||||
{t("comm.channels")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onHideSidebar}
|
||||
className="rounded-[4px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
|
||||
aria-label="Hide sidebar"
|
||||
>
|
||||
<CaretDown size={12} className="-rotate-90" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-2 py-2">
|
||||
<div className="flex-1 overflow-y-auto px-2 py-3">
|
||||
{channelGroups.map((group) => (
|
||||
<div key={group.type} className="mb-3">
|
||||
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
||||
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{group.label}
|
||||
</div>
|
||||
{group.items.map((ch) => {
|
||||
@@ -152,7 +136,7 @@ export function ChannelSidebar({
|
||||
|
||||
{taskDiscussionChannels.length > 0 ? (
|
||||
<div className="mb-3">
|
||||
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
||||
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{t("comm.taskDiscussions")}
|
||||
</div>
|
||||
{taskDiscussionChannels.map((discussion) => (
|
||||
@@ -164,10 +148,10 @@ export function ChannelSidebar({
|
||||
selectedChannelKey === discussion.key ? "bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]" style={{ backgroundColor: `${TERRACOTTA}18` }}>
|
||||
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: TERRACOTTA }} />
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]" style={{ backgroundColor: "var(--accent-soft)" }}>
|
||||
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--accent)" }} />
|
||||
</div>
|
||||
<span className={`min-w-0 flex-1 truncate text-[12px] ${selectedChannelKey === discussion.key ? "font-semibold text-[color:var(--text)]" : "font-medium text-[color:var(--muted-strong)]"}`}>
|
||||
<span className={`min-w-0 flex-1 truncate text-[13px] ${selectedChannelKey === discussion.key ? "font-semibold text-[color:var(--text)]" : "font-medium text-[color:var(--muted-strong)]"}`}>
|
||||
{discussion.label}
|
||||
</span>
|
||||
</button>
|
||||
@@ -183,7 +167,7 @@ export function ChannelSidebar({
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[color:var(--line)] px-4 py-2">
|
||||
<div className="flex items-center justify-between text-[10px] text-[color:var(--muted)]">
|
||||
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
|
||||
<span>{totalChannels} channels</span>
|
||||
<span>{agents.length} agents</span>
|
||||
</div>
|
||||
@@ -225,21 +209,21 @@ export function FilterBar({
|
||||
onKeyDown={onSearchKeyDown}
|
||||
placeholder={t("comm.searchMessages")}
|
||||
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
|
||||
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
|
||||
style={{ focusRingColor: "var(--accent)" } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
{scopeToggle}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className={`flex items-center gap-1.5 rounded-[6px] border px-2.5 py-1.5 text-[10px] font-medium transition ${
|
||||
className={`flex items-center gap-1.5 rounded-[6px] border px-2.5 py-1.5 text-[11px] font-medium transition ${
|
||||
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
|
||||
? "border-transparent text-white"
|
||||
? "border-transparent text-[color:var(--text-on-accent)]"
|
||||
: "border-[color:var(--line)] text-[color:var(--muted-strong)] hover:border-[color:var(--line-strong)]"
|
||||
}`}
|
||||
style={
|
||||
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
|
||||
? { backgroundColor: TERRACOTTA }
|
||||
? { backgroundColor: "var(--accent)" }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
@@ -259,7 +243,7 @@ export function FilterBar({
|
||||
<select
|
||||
value={filters.channelType}
|
||||
onChange={(e) => onChangeFilters({ ...filters, channelType: e.target.value as MessageChannel | "all" })}
|
||||
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
|
||||
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
|
||||
>
|
||||
<option value="all">{t("comm.allChannels")}</option>
|
||||
<option value="direct">{t("comm.direct")}</option>
|
||||
@@ -271,7 +255,7 @@ export function FilterBar({
|
||||
<select
|
||||
value={filters.priority}
|
||||
onChange={(e) => onChangeFilters({ ...filters, priority: e.target.value })}
|
||||
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
|
||||
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
|
||||
>
|
||||
<option value="all">{t("comm.allPriorities")}</option>
|
||||
<option value="urgent">{t("comm.urgent")}</option>
|
||||
@@ -281,10 +265,10 @@ export function FilterBar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
|
||||
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition ${
|
||||
filters.unreadOnly ? "text-white" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${
|
||||
filters.unreadOnly ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
}`}
|
||||
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
|
||||
style={filters.unreadOnly ? { backgroundColor: "var(--accent)" } : undefined}
|
||||
>
|
||||
{t("comm.unreadOnly")}
|
||||
</button>
|
||||
@@ -292,7 +276,7 @@ export function FilterBar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChangeFilters({ search: filters.search, channelType: "all", priority: "all", unreadOnly: false })}
|
||||
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
|
||||
className="text-[11px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
|
||||
>
|
||||
{t("comm.clearFilters")}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { ChatCircle, Plus } from "@phosphor-icons/react"
|
||||
import type { AgentMessageRecord, AgentRecord, CommentRecord, MessageChannel, ProjectRecord, TaskRecord } from "@shared/types"
|
||||
import { useAppStore } from "../../state/store"
|
||||
import { unwrap } from "../../lib/desktop"
|
||||
@@ -9,12 +8,10 @@ import { ChannelSidebar, FilterBar, type FilterState } from "./ChannelSidebar"
|
||||
import type { ChannelDescriptor, TaskDiscussionDescriptor, AnyChannel } from "./ChannelHeader"
|
||||
import { ChannelHeader } from "./ChannelHeader"
|
||||
import { MessageList } from "./MessageList"
|
||||
import { MessageComposer, TaskDiscussionComposer, NoChannelComposer, ComposeModal } from "./MessageComposer"
|
||||
import { MessageComposer, TaskDiscussionComposer, NoChannelComposer } from "./MessageComposer"
|
||||
import { SearchResults } from "./SearchResults"
|
||||
import { deriveChannels, deriveTaskDiscussionChannels, getChannelMessages } from "./channel-utils"
|
||||
|
||||
const TERRACOTTA = "#d97745"
|
||||
const TERRACOTTA_SOFT = "#d9774518"
|
||||
|
||||
interface CommunicationHubProps {
|
||||
messages: AgentMessageRecord[]; comments: CommentRecord[]; tasks: TaskRecord[]; agents: AgentRecord[]
|
||||
@@ -35,9 +32,9 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
|
||||
aria-checked={scope === "global"}
|
||||
onClick={() => onChangeScope("global")}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("global") } }}
|
||||
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition-colors duration-150 ${
|
||||
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
|
||||
scope === "global"
|
||||
? "bg-[color:var(--accent)] text-white"
|
||||
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
|
||||
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
}`}
|
||||
>
|
||||
@@ -49,9 +46,9 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
|
||||
aria-checked={scope === "channel"}
|
||||
onClick={() => onChangeScope("channel")}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("channel") } }}
|
||||
className={`rounded-full px-2.5 py-1 text-[10px] font-semibold transition-colors duration-150 ${
|
||||
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
|
||||
scope === "channel"
|
||||
? "bg-[color:var(--accent)] text-white"
|
||||
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
|
||||
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
}`}
|
||||
>
|
||||
@@ -65,7 +62,6 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
||||
const t = useT()
|
||||
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
|
||||
const [showSidebar, setShowSidebar] = useState(true)
|
||||
const [showCompose, setShowCompose] = useState(false)
|
||||
const [replyTo, setReplyTo] = useState<AgentMessageRecord | null>(null)
|
||||
const [optimisticReadIds, setOptimisticReadIds] = useState<Set<string>>(new Set())
|
||||
const pendingReadIdsRef = useRef<Set<string>>(new Set())
|
||||
@@ -218,7 +214,6 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
||||
return counts
|
||||
}, [channels, effectiveMessages])
|
||||
|
||||
const totalUnread = useMemo(() => effectiveMessages.filter((m) => !m.readAt).length, [effectiveMessages])
|
||||
|
||||
const channelGroups = useMemo(() => {
|
||||
const groups: Array<{ type: MessageChannel; label: string; items: ChannelDescriptor[] }> = []
|
||||
@@ -235,64 +230,47 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[10px]" style={{ backgroundColor: TERRACOTTA_SOFT }}>
|
||||
<ChatCircle size={20} weight="fill" style={{ color: TERRACOTTA }} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("comm.communicationHub")}</h2>
|
||||
<p className="text-[12px] text-[color:var(--muted)]">{messages.length} message{messages.length !== 1 ? "s" : ""}{totalUnread > 0 ? <span style={{ color: TERRACOTTA }}> ({totalUnread} unread)</span> : null}</p>
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 overflow-hidden">
|
||||
<AnimatePresence initial={false}>
|
||||
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={setSelectedChannelKey} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
|
||||
</AnimatePresence>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
|
||||
<div className="flex-1 overflow-y-auto px-2 py-3">
|
||||
<AnimatePresence mode="wait">
|
||||
{isSearchActive ? (
|
||||
<motion.div
|
||||
key="search-results"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: "easeInOut" }}
|
||||
className="h-full"
|
||||
>
|
||||
<SearchResults
|
||||
results={searchResults}
|
||||
query={searchQuery}
|
||||
loading={searchLoading}
|
||||
agents={agents}
|
||||
onNavigateToResult={handleNavigateToResult}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="message-list"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: "easeInOut" }}
|
||||
className="h-full"
|
||||
>
|
||||
<MessageList activeChannel={activeChannel} filteredMessages={filteredMessages} taskDiscussionComments={taskComments} activeTask={activeTask} agentMap={agentMap} filters={filters} onReply={setReplyTo} messageCount={effectiveMessages.length} selectedChannelKey={selectedChannelKey} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<button type="button" onClick={() => setShowCompose(true)} className="inline-flex items-center gap-2 rounded-[8px] px-3 py-2 text-[11px] font-semibold text-white transition hover:brightness-90" style={{ backgroundColor: TERRACOTTA }}>
|
||||
<Plus size={13} weight="bold" />{t("comm.newMessage")}
|
||||
</button>
|
||||
{activeChannel ? (activeChannel.kind === "message" ? <MessageComposer agents={agents} companyId={companyId} channel={activeChannel.channel} channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId} directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []} replyTo={replyTo} onSent={() => { setReplyTo(null); onRefresh() }} onCancelReply={() => setReplyTo(null)} /> : <TaskDiscussionComposer companyId={companyId} taskId={activeChannel.taskId} onSent={onRefresh} />) : <NoChannelComposer />}
|
||||
</div>
|
||||
<div className="flex overflow-hidden rounded-[12px] border border-[color:var(--line)]" style={{ height: "calc(100vh - 220px)", minHeight: "480px" }}>
|
||||
<AnimatePresence initial={false}>
|
||||
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={setSelectedChannelKey} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
|
||||
</AnimatePresence>
|
||||
<div className="flex min-w-0 flex-1 flex-col bg-[color:var(--bg)]">
|
||||
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
|
||||
<div className="flex-1 overflow-y-auto px-2 py-3">
|
||||
<AnimatePresence mode="wait">
|
||||
{isSearchActive ? (
|
||||
<motion.div
|
||||
key="search-results"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: "easeInOut" }}
|
||||
className="h-full"
|
||||
>
|
||||
<SearchResults
|
||||
results={searchResults}
|
||||
query={searchQuery}
|
||||
loading={searchLoading}
|
||||
agents={agents}
|
||||
onNavigateToResult={handleNavigateToResult}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="message-list"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: "easeInOut" }}
|
||||
className="h-full"
|
||||
>
|
||||
<MessageList activeChannel={activeChannel} filteredMessages={filteredMessages} taskDiscussionComments={taskComments} activeTask={activeTask} agentMap={agentMap} filters={filters} onReply={setReplyTo} messageCount={effectiveMessages.length} selectedChannelKey={selectedChannelKey} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{activeChannel ? (activeChannel.kind === "message" ? <MessageComposer agents={agents} companyId={companyId} channel={activeChannel.channel} channelTargetId={activeChannel.channel === "direct" ? null : activeChannel.targetId} directParticipantIds={activeChannel.channel === "direct" ? activeChannel.participantIds : []} replyTo={replyTo} onSent={() => { setReplyTo(null); onRefresh() }} onCancelReply={() => setReplyTo(null)} /> : <TaskDiscussionComposer companyId={companyId} taskId={activeChannel.taskId} onSent={onRefresh} />) : <NoChannelComposer />}
|
||||
</div>
|
||||
</div>
|
||||
<ComposeModal open={showCompose} onClose={() => setShowCompose(false)} agents={agents} projects={projects} companyId={companyId} onSent={onRefresh} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,11 +7,9 @@ import { InlineNotice, Modal, Select, Input, TextArea } from "../ui"
|
||||
import { AgentAvatar } from "./MessageItem"
|
||||
import { useT } from "../../i18n"
|
||||
|
||||
const TERRACOTTA = "#d97745"
|
||||
|
||||
function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) {
|
||||
return (
|
||||
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" style={{ backgroundColor: TERRACOTTA }} aria-label={label}>
|
||||
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" aria-label={label}>
|
||||
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={18} weight="fill" />}
|
||||
</button>
|
||||
)
|
||||
@@ -57,7 +55,7 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
|
||||
<AnimatePresence>
|
||||
{replyTo ? (
|
||||
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="mb-2 flex items-center gap-2 overflow-hidden rounded-[6px] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<ArrowBendUpLeft size={12} style={{ color: TERRACOTTA }} />
|
||||
<ArrowBendUpLeft size={12} className="text-[color:var(--accent)]" />
|
||||
<span className="min-w-0 flex-1 truncate text-[11px] text-[color:var(--muted-strong)]">Replying to <span className="font-semibold">{agents.find((a) => a.id === replyTo.fromAgentId)?.name ?? "agent"}</span>{replyTo.subject ? `: ${replyTo.subject}` : ""}</span>
|
||||
<button type="button" onClick={onCancelReply} className="shrink-0 rounded-[4px] p-0.5 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={12} /></button>
|
||||
</motion.div>
|
||||
@@ -71,10 +69,10 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
|
||||
{!replyTo ? <input type="text" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Subject (optional)" className="min-w-0 flex-1 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" /> : null}
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[12px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: TERRACOTTA } as React.CSSProperties} />
|
||||
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
|
||||
<SendButton sending={sending} disabled={!body.trim() || !fromAgentId || (channel === "direct" && !directRecipientId)} onClick={() => void handleSend()} label="Send message" />
|
||||
</div>
|
||||
<div className="mt-1 text-[9px] text-[color:var(--muted)]">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -89,7 +87,7 @@ export function TaskDiscussionComposer({ companyId, taskId, onSent }: { companyI
|
||||
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[12px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: TERRACOTTA } as React.CSSProperties} />
|
||||
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
|
||||
<SendButton sending={sending} disabled={!body.trim()} onClick={() => void handleSend()} label="Send task comment" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,8 +130,8 @@ export function ComposeModal({ open, onClose, agents, projects, companyId, onSen
|
||||
<TextArea label={t("comm.message")} value={body} onChange={setBody} placeholder={t("comm.writeMessage")} rows={5} />
|
||||
<Select label="Priority" value={priority} onChange={(v) => setPriority(v as MessagePriority)} options={[{ value: "low", label: "Low" }, { value: "normal", label: "Normal" }, { value: "urgent", label: "Urgent" }]} />
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onClick={onClose} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">{t("common.cancel")}</button>
|
||||
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-[8px] px-4 py-2 text-[12px] font-semibold text-white transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" style={{ backgroundColor: TERRACOTTA }}>
|
||||
<button type="button" onClick={onClose} className="rounded-[8px] px-3 py-2 text-[13px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">{t("common.cancel")}</button>
|
||||
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] font-semibold bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none">
|
||||
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={14} weight="fill" />}{t("comm.sendMessage")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import {
|
||||
ChatCircle,
|
||||
@@ -11,7 +12,6 @@ import type { AgentMessageRecord, AgentRecord, CommentRecord, DepartmentId } fro
|
||||
import { getAgentInitials, formatMessageTime } from "../../lib/formatters"
|
||||
import { DEPARTMENT_COLORS } from "@shared/constants"
|
||||
|
||||
const TERRACOTTA = "#d97745"
|
||||
|
||||
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
|
||||
urgent: { label: "Urgent", color: "var(--danger)", bg: "var(--danger-soft)" },
|
||||
@@ -24,7 +24,7 @@ function getDepartmentColor(department: DepartmentId | null | undefined): string
|
||||
return DEPARTMENT_COLORS[department] ?? "var(--muted)"
|
||||
}
|
||||
|
||||
export function AgentAvatar({
|
||||
export const AgentAvatar = memo(function AgentAvatar({
|
||||
agent,
|
||||
size = "md",
|
||||
}: {
|
||||
@@ -43,9 +43,9 @@ export function AgentAvatar({
|
||||
{initials}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function MessageBubble({
|
||||
export const MessageBubble = memo(function MessageBubble({
|
||||
message,
|
||||
agent,
|
||||
isThread,
|
||||
@@ -67,7 +67,7 @@ export function MessageBubble({
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`group relative flex gap-3 rounded-[10px] px-4 py-3 transition ${
|
||||
className={`group relative flex gap-3 rounded-[8px] px-4 py-3 transition ${
|
||||
isThread ? "ml-11" : ""
|
||||
} ${
|
||||
isUnread
|
||||
@@ -82,21 +82,21 @@ export function MessageBubble({
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-semibold" style={{ color: deptColor }}>
|
||||
<span className="truncate text-[13px] font-semibold" style={{ color: deptColor }}>
|
||||
{agent?.name ?? "Unknown Agent"}
|
||||
</span>
|
||||
{agent?.title ? (
|
||||
<span className="text-[10px] text-[color:var(--muted)]">{agent.title}</span>
|
||||
<span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span>
|
||||
) : null}
|
||||
{agent?.department ? (
|
||||
<span
|
||||
className="rounded-full px-1.5 py-0.5 text-[8px] font-bold uppercase tracking-[0.08em]"
|
||||
className="shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em]"
|
||||
style={{ backgroundColor: `${deptColor}15`, color: deptColor }}
|
||||
>
|
||||
{agent.department.replace("_", " ")}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-[10px] text-[color:var(--muted)]">
|
||||
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">
|
||||
{formatMessageTime(message.createdAt)}
|
||||
</span>
|
||||
{message.priority !== "normal" ? (
|
||||
@@ -112,23 +112,23 @@ export function MessageBubble({
|
||||
</span>
|
||||
) : null}
|
||||
{isUnread ? (
|
||||
<Circle size={8} weight="fill" className="shrink-0" style={{ color: TERRACOTTA }} />
|
||||
<Circle size={8} weight="fill" className="shrink-0" style={{ color: "var(--accent)" }} aria-label="Unread" />
|
||||
) : (
|
||||
<CheckCircle
|
||||
size={10}
|
||||
weight="fill"
|
||||
className="shrink-0 text-[color:var(--muted)] opacity-0 group-hover:opacity-60"
|
||||
className="shrink-0 text-[color:var(--muted)] opacity-0 group-hover:opacity-60 group-focus-within:opacity-60"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message.subject ? (
|
||||
<div className="mt-0.5 text-[13px] font-medium text-[color:var(--text)]">
|
||||
<div className="mt-0.5 break-words text-[13px] font-medium text-[color:var(--text)]">
|
||||
{message.subject}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
|
||||
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
|
||||
{message.body}
|
||||
</div>
|
||||
|
||||
@@ -142,7 +142,7 @@ export function MessageBubble({
|
||||
type="button"
|
||||
onClick={onReply}
|
||||
className="flex items-center gap-1.5 text-[11px] font-medium transition hover:opacity-80"
|
||||
style={{ color: TERRACOTTA }}
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
<ChatCircle size={12} />
|
||||
{replyCount} {replyCount === 1 ? "reply" : "replies"}
|
||||
@@ -151,7 +151,7 @@ export function MessageBubble({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReply}
|
||||
className="flex items-center gap-1 rounded-[5px] px-2 py-0.5 text-[10px] font-medium text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] group-hover:opacity-100"
|
||||
className="flex items-center gap-1 rounded-[6px] px-2 py-0.5 text-[11px] font-medium text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] group-hover:opacity-100"
|
||||
>
|
||||
<ArrowBendUpLeft size={10} />
|
||||
Reply
|
||||
@@ -160,7 +160,7 @@ export function MessageBubble({
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function TaskCommentBubble({
|
||||
comment,
|
||||
@@ -177,19 +177,19 @@ export function TaskCommentBubble({
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.18 }}
|
||||
className="group relative flex gap-3 rounded-[10px] border-l-[3px] bg-[color:var(--panel)] px-4 py-3"
|
||||
style={{ borderLeftColor: isBoard ? TERRACOTTA : deptColor }}
|
||||
className="group relative flex gap-3 rounded-[8px] border-l-[3px] bg-[color:var(--panel)] px-4 py-3"
|
||||
style={{ borderLeftColor: isBoard ? "var(--accent)" : deptColor }}
|
||||
>
|
||||
<AgentAvatar agent={agent} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-semibold" style={{ color: isBoard ? TERRACOTTA : deptColor }}>
|
||||
<span className="truncate text-[13px] font-semibold" style={{ color: isBoard ? "var(--accent)" : deptColor }}>
|
||||
{comment.authorName}
|
||||
</span>
|
||||
{agent?.title ? <span className="text-[10px] text-[color:var(--muted)]">{agent.title}</span> : null}
|
||||
<span className="text-[10px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span>
|
||||
{agent?.title ? <span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span> : null}
|
||||
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
|
||||
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
|
||||
{comment.body}
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ function AttachmentList({ json }: { json: string }) {
|
||||
{items.map((item, i) => (
|
||||
<span
|
||||
key={`${item.id}-${i}`}
|
||||
className="inline-flex items-center gap-1 rounded-[5px] bg-[color:var(--panel-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--muted-strong)]"
|
||||
className="inline-flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] px-2 py-1 text-[11px] font-medium text-[color:var(--muted-strong)]"
|
||||
>
|
||||
<At size={10} className="text-[color:var(--muted)]" />
|
||||
{item.title || `${item.type}:${item.id.slice(0, 8)}`}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function MessageList({
|
||||
: t("comm.noMessagesChannel")}
|
||||
</div>
|
||||
{(filters.search || filters.unreadOnly || filters.priority !== "all") ? (
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("comm.adjustFilters")}</div>
|
||||
<div className="mt-1 text-[13px] text-[color:var(--muted)]">{t("comm.adjustFilters")}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,9 +10,9 @@ import { useRovingTabIndex } from "../../lib/keyboard"
|
||||
|
||||
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
|
||||
direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--accent)" },
|
||||
department: { label: "Department", icon: Buildings, color: "#6366f1" },
|
||||
company: { label: "Company", icon: Megaphone, color: "#d97745" },
|
||||
project: { label: "Project", icon: Hash, color: "#10b981" },
|
||||
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
|
||||
company: { label: "Company", icon: Megaphone, color: "var(--accent)" },
|
||||
project: { label: "Project", icon: Hash, color: "var(--success)" },
|
||||
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
|
||||
}
|
||||
|
||||
@@ -152,28 +152,28 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Row 1: Agent name, channel badge, timestamp */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-[color:var(--text)]">
|
||||
<span className="truncate text-[11px] font-semibold text-[color:var(--text)]">
|
||||
{agent?.name ?? "Unknown Agent"}
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium"
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium"
|
||||
style={{ backgroundColor: `${config.color}18`, color: config.color }}
|
||||
>
|
||||
<ChannelIcon size={10} weight="bold" />
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 font-mono text-[10px] text-[color:var(--muted)]">
|
||||
<span className="ml-auto whitespace-nowrap shrink-0 font-mono text-[11px] text-[color:var(--muted)]">
|
||||
{formatMessageTime(message.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Subject */}
|
||||
<div className="mt-0.5 truncate text-[12px] font-semibold text-[color:var(--text)]">
|
||||
<div className="mt-0.5 truncate text-[13px] font-semibold text-[color:var(--text)]">
|
||||
{highlightMatch(message.subject, query)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Body snippet */}
|
||||
<div className="mt-0.5 line-clamp-2 text-[12px] leading-relaxed text-[color:var(--muted)]">
|
||||
<div className="mt-0.5 line-clamp-2 break-words text-[13px] leading-relaxed text-[color:var(--muted)]">
|
||||
{highlightMatch(message.body, query)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,8 +89,8 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusPill status={document.status} />
|
||||
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
|
||||
<button type="button" aria-label="Edit document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" aria-label="Delete document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</ListRow>
|
||||
|
||||
@@ -77,8 +77,8 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusPill status={entry.importance} />
|
||||
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
|
||||
<button type="button" aria-label="Edit entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
|
||||
<button type="button" aria-label="Delete entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</ListRow>
|
||||
|
||||
@@ -137,7 +137,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
|
||||
<div className="grid gap-4">
|
||||
{submitError ? <InlineNotice message={submitError} /> : null}
|
||||
{needsLogin ? (
|
||||
<div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-4 text-[12px] text-[color:var(--warn)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-4 text-[12px] text-[color:var(--warn)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em]">{authActionCopy.title}</div>
|
||||
<div className="mt-1.5 leading-relaxed">{authActionCopy.message}</div>
|
||||
<div className="mt-3">
|
||||
@@ -249,7 +249,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
|
||||
disabled={requiresExplicitModel && !model.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-4 text-[12px] leading-relaxed text-[color:var(--muted)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-4 text-[12px] leading-relaxed text-[color:var(--muted)]">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Configuration doc</div>
|
||||
<pre className="mono whitespace-pre-wrap">{connector.configurationDoc}</pre>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
|
||||
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="rounded-[10px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
|
||||
className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
@@ -69,11 +69,12 @@ export function BudgetAlertBanner({ agents, onNavigate }: BudgetAlertBannerProps
|
||||
{agentsOverThreshold.map((agent) => (
|
||||
<span
|
||||
key={agent.id}
|
||||
className={`text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
agent.pct >= 100
|
||||
? "text-[color:var(--danger)]"
|
||||
: "text-[color:var(--warn)]"
|
||||
}`}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{agent.name} ({agent.pct}%)
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
ArrowRight,
|
||||
Briefcase,
|
||||
@@ -28,6 +29,7 @@ import { RecoveryBanner } from "./RecoveryBanner";
|
||||
import { BudgetAlertBanner } from "./BudgetAlertBanner";
|
||||
|
||||
export interface DashboardProps {
|
||||
companyName?: string;
|
||||
agents: AgentRecord[];
|
||||
tasks: TaskRecord[];
|
||||
runs: RunRecord[];
|
||||
@@ -42,7 +44,10 @@ export interface DashboardProps {
|
||||
|
||||
export function Dashboard(props: DashboardProps) {
|
||||
const t = useT();
|
||||
const { agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
|
||||
const { companyName, agents, tasks, runs, approvals, activity, costs, messages = [], onNavigate } = props;
|
||||
|
||||
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
|
||||
const taskMap = useMemo(() => new Map(tasks.map((t) => [t.id, t])), [tasks]);
|
||||
|
||||
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
|
||||
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
|
||||
@@ -59,15 +64,35 @@ export function Dashboard(props: DashboardProps) {
|
||||
return runs
|
||||
.filter((r) => r.status === "running" || r.status === "queued")
|
||||
.map((run) => {
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agentMap.get(run.agentId);
|
||||
const task = taskMap.get(run.taskId);
|
||||
return { run, agent, task };
|
||||
})
|
||||
.slice(0, 8);
|
||||
}, [runs, agents, tasks]);
|
||||
}, [runs, agentMap, taskMap]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
{/* Company status line */}
|
||||
{companyName ? (
|
||||
<div>
|
||||
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{companyName}</h2>
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-[color:var(--muted)]">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
|
||||
{activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Conditional banners */}
|
||||
<RecoveryBanner />
|
||||
<BudgetAlertBanner agents={agents} onNavigate={onNavigate} />
|
||||
@@ -109,15 +134,19 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Needs Attention — moved to top, only shows when items exist */}
|
||||
{hasAttentionItems ? (
|
||||
<section>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--warn)]" />
|
||||
{t("dash.needsAttentionSection")}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{pendingApprovals.slice(0, 5).map((approval) => {
|
||||
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
|
||||
{pendingApprovals.slice(0, 5).map((approval, i) => {
|
||||
const requester = approval.requestedByAgentId ? agentMap.get(approval.requestedByAgentId) : undefined;
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
key={approval.id}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", approval.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
@@ -131,11 +160,11 @@ export function Dashboard(props: DashboardProps) {
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{failedRuns.slice(0, 3).map((run) => {
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const agent = agentMap.get(run.agentId);
|
||||
return (
|
||||
<button
|
||||
key={run.id}
|
||||
@@ -155,7 +184,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
);
|
||||
})}
|
||||
{urgentMessages.slice(0, 3).map((msg) => {
|
||||
const sender = agents.find((a) => a.id === msg.fromAgentId);
|
||||
const sender = agentMap.get(msg.fromAgentId);
|
||||
return (
|
||||
<button
|
||||
key={msg.id}
|
||||
@@ -189,7 +218,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
</button>
|
||||
</div>
|
||||
{activeWork.length > 0 ? (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||
{activeWork.map(({ run, agent, task }) => (
|
||||
<button
|
||||
key={run.id}
|
||||
@@ -201,7 +230,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<Robot size={16} className="text-[color:var(--success)]" />
|
||||
{run.status === "running" ? (
|
||||
<span className="absolute -right-0.5 -top-0.5 h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -218,7 +247,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[10px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||
<div className="rounded-[8px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div>
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div>
|
||||
</div>
|
||||
@@ -232,7 +261,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
{t("dash.recentActivity")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||
<ActivityFeed activity={activity} agents={agents} limit={10} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,12 +19,12 @@ const DEPARTMENT_INFO: Record<string, { label: string; icon: React.ElementType;
|
||||
engineering: { label: "Engineering", icon: Cpu, color: "#6366f1" },
|
||||
product: { label: "Product", icon: Rocket, color: "#8b5cf6" },
|
||||
design: { label: "Design", icon: Target, color: "#ec4899" },
|
||||
marketing: { label: "Marketing", icon: TrendUp, color: "#f59e0b" },
|
||||
marketing: { label: "Marketing", icon: TrendUp, color: "#0ea5e9" },
|
||||
sales: { label: "Sales", icon: ChartLineUp, color: "#10b981" },
|
||||
hr: { label: "People", icon: Users, color: "#06b6d4" },
|
||||
finance: { label: "Finance", icon: CurrencyCircleDollar, color: "#14b8a6" },
|
||||
legal: { label: "Legal", icon: ShieldCheck, color: "#64748b" },
|
||||
operations: { label: "Operations", icon: Lightning, color: "#f97316" },
|
||||
operations: { label: "Operations", icon: Lightning, color: "#475569" },
|
||||
customer_support: { label: "Support", icon: Heartbeat, color: "#3b82f6" },
|
||||
research: { label: "Research", icon: Target, color: "#a855f7" },
|
||||
};
|
||||
@@ -88,12 +88,12 @@ export function DepartmentOverview({
|
||||
key={deptId}
|
||||
type="button"
|
||||
onClick={() => onNavigate("agents")}
|
||||
className="focus-ring group relative overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className="focus-ring group relative overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-[3px]" style={{ backgroundColor: info.color }} />
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<Icon size={14} style={{ color: info.color }} />
|
||||
<span className="text-[11px] font-semibold text-[color:var(--text)]">{info.label}</span>
|
||||
<Icon size={15} weight="fill" style={{ color: info.color }} />
|
||||
<span className="truncate text-[11px] font-medium text-[color:var(--text)]">{info.label}</span>
|
||||
{runningAgents > 0 && (
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
|
||||
@@ -103,18 +103,18 @@ export function DepartmentOverview({
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-1 pl-1">
|
||||
<div>
|
||||
<div className="text-[14px] font-bold text-[color:var(--text)]">{data.agents.length}</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">People</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--text)]">{data.agents.length}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">People</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-bold text-[color:var(--accent)]">{activeTasks}</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">Active</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--accent)]">{activeTasks}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Active</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-[14px] font-bold ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
|
||||
<div className={`text-[16px] font-bold tabular-nums ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
|
||||
{blockedTasks > 0 ? blockedTasks : doneTasks}
|
||||
</div>
|
||||
<div className="text-[8px] uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
{blockedTasks > 0 ? "Blocked" : "Done"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Target } from "@phosphor-icons/react";
|
||||
import type { GoalRecord, SectionId, TaskRecord } from "@shared/types";
|
||||
|
||||
@@ -21,24 +20,22 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
|
||||
key={goal.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("goals")}
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)]"
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={14} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[12px] font-medium text-[color:var(--text)]">{goal.title}</span>
|
||||
<Target size={15} weight="fill" className="text-[color:var(--accent)]" />
|
||||
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-[color:var(--accent)]">{pct}%</span>
|
||||
<span className="whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
|
||||
</div>
|
||||
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-[color:var(--accent)]"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<div
|
||||
className="h-full rounded-full bg-[color:var(--accent)] transition-all duration-700 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{done}/{total} tasks complete</div>
|
||||
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>{done}/{total} tasks complete</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
|
||||
@@ -57,11 +57,11 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
{KANBAN_COLUMNS.map((col) => {
|
||||
const colTasks = tasksByStatus[col.status] ?? [];
|
||||
return (
|
||||
<div key={col.status} className="flex w-[220px] shrink-0 flex-col">
|
||||
<div key={col.status} className="flex w-[220px] min-w-[180px] shrink-0 flex-col">
|
||||
<div className="mb-2 flex items-center gap-2 px-1">
|
||||
<span className={`h-2 w-2 rounded-full ${col.dotColor}`} />
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
|
||||
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
|
||||
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[10px] font-bold text-[color:var(--muted)]">
|
||||
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-bold tabular-nums text-[color:var(--muted-strong)]">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -75,9 +75,9 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
key={task.id}
|
||||
type="button"
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
>
|
||||
<div className="text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="truncate text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
{assignee ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
|
||||
@@ -88,8 +88,8 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
|
||||
)}
|
||||
{isRunning ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-[color:var(--success)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-[color:var(--success)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" aria-hidden="true" />
|
||||
Running
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -30,19 +30,27 @@ export function MetricCard({
|
||||
accent: "bg-[color:var(--accent-soft)]",
|
||||
};
|
||||
|
||||
const borderMap = {
|
||||
default: "border-l-transparent",
|
||||
success: "border-l-[color:var(--success)]",
|
||||
warn: "border-l-[color:var(--warn)]",
|
||||
danger: "border-l-[color:var(--danger)]",
|
||||
accent: "border-l-[color:var(--accent)]",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="focus-ring flex items-center gap-3.5 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className={`focus-ring flex items-center gap-4 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${borderMap[tone]}`}
|
||||
>
|
||||
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
|
||||
<Icon size={20} weight="fill" className={toneMap[tone]} />
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
|
||||
<Icon size={24} weight="fill" className={toneMap[tone]} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className={`text-[20px] font-bold tracking-[-0.02em] leading-tight ${toneMap[tone]}`}>{value}</div>
|
||||
{subtitle ? <div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className={`text-[28px] font-bold tracking-[-0.02em] leading-none ${toneMap[tone]}`} style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
|
||||
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -121,7 +121,7 @@ export function NeedsAttention({
|
||||
<Cpu size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{action.resultSummary || action.actionType}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
@@ -136,7 +136,7 @@ export function NeedsAttention({
|
||||
<Lightning size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{workflow.name}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
</button>
|
||||
|
||||
@@ -20,17 +20,17 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3.5 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[14px] font-bold ${
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-full text-[15px] font-bold ${
|
||||
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
}`}>
|
||||
{initials}
|
||||
</div>
|
||||
{agent.status === "running" ? (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
|
||||
</span>
|
||||
) : (
|
||||
@@ -61,7 +61,7 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-10 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-10 text-center">
|
||||
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function RecoveryBanner() {
|
||||
const handleDismissAll = () => setDismissed(true);
|
||||
|
||||
return (
|
||||
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[10px] p-4 mb-4">
|
||||
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[8px] px-4 py-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-[color:var(--warn)] font-medium">
|
||||
<Warning size={18} weight="bold" />
|
||||
@@ -53,10 +53,10 @@ export function RecoveryBanner() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{runs.map(run => (
|
||||
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded px-3 py-2 text-sm">
|
||||
<div>
|
||||
<span className="text-[color:var(--text)]">{run.taskTitle}</span>
|
||||
<span className="text-[color:var(--muted)] ml-2">({run.agentName})</span>
|
||||
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded-[4px] px-3 py-2 text-[13px]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="truncate text-[color:var(--text)]">{run.taskTitle}</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--muted)] ml-2">({run.agentName})</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
|
||||
@@ -30,11 +30,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
|
||||
}, [tasks]);
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarCheck size={16} className="text-[color:var(--accent)]" />
|
||||
<span className="text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
|
||||
<span className="truncate text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
|
||||
<StatusPill status={sprint.status} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-[color:var(--muted)]">
|
||||
@@ -53,11 +53,11 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between text-[10px] text-[color:var(--muted)]">
|
||||
<span>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
|
||||
<span style={{ fontVariantNumeric: "tabular-nums" }}>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[color:var(--success)]">{sprintTasks.done} done</span>
|
||||
<span className="text-[color:var(--accent)]">{sprintTasks.active} active</span>
|
||||
<span>{sprintTasks.total} total</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--success)]">{sprintTasks.done} done</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--accent)]">{sprintTasks.active} active</span>
|
||||
<span className="whitespace-nowrap">{sprintTasks.total} total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
|
||||
{stages.map((stage, i) => (
|
||||
<div key={stage.label} className="flex items-center gap-1.5">
|
||||
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
|
||||
<span className={`text-[18px] font-bold ${stage.tone}`}>{stage.count}</span>
|
||||
<span className={`text-[18px] font-bold ${stage.tone}`} style={{ fontVariantNumeric: "tabular-nums" }}>{stage.count}</span>
|
||||
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
|
||||
</div>
|
||||
{i < stages.length - 1 ? (
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
|
||||
overscan: 15,
|
||||
})
|
||||
|
||||
if (activity.length === 0) return <EmptyState title={t("exec.noActivityYet")} />
|
||||
if (activity.length === 0) return <EmptyState title={t("exec.noActivityYet")} detail={t("exec.noActivityDetail")} />
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -81,19 +81,19 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<EntityIcon size={14} weight="duotone" className="text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] text-[color:var(--text)]">{entry.action}</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EntityIcon size={14} weight="duotone" className="shrink-0 text-[color:var(--muted)]" />
|
||||
<div className="truncate text-[13px] text-[color:var(--text)]">{entry.action}</div>
|
||||
</div>
|
||||
<div className="mono text-[11px] text-[color:var(--muted)]">{formatTime(entry.createdAt)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[13px] text-[color:var(--muted)]">
|
||||
<span className="font-medium text-[color:var(--muted-strong)]">{entry.actor}</span>
|
||||
<span>·</span>
|
||||
<span>{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
|
||||
<span className="truncate">{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { unwrap } from "../../lib/desktop"
|
||||
import { ActionButton, ConfirmDialog, InlineNotice, StatusPill, TextArea } from "../ui"
|
||||
import { describeActionError, parseApprovalPayloadJson } from "./execution-utils"
|
||||
import { SocialDraftPreview } from "./SocialDraftPreview"
|
||||
import { useAppStore } from "../../state/store"
|
||||
|
||||
function getTypeBadgeClass(type: string): string {
|
||||
if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
|
||||
@@ -21,6 +22,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>; onDeleted: () => Promise<void>
|
||||
}) {
|
||||
const t = useT()
|
||||
const addToast = useAppStore((s) => s.addToast)
|
||||
const [decisionNote, setDecisionNote] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false)
|
||||
const [actionError, setActionError] = useState<string | null>(null); const [acting, setActing] = useState<"approved"|"rejected"|"revision_requested"|"delete"|null>(null)
|
||||
const [scheduleMode, setScheduleMode] = useState<"immediate" | "scheduled">("immediate")
|
||||
@@ -59,6 +61,9 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo
|
||||
}
|
||||
await onDecide(state, noteForDecision)
|
||||
if (state === "approved") addToast("Approval granted — agent is moving forward", "success")
|
||||
else if (state === "rejected") addToast("Approval rejected", "warn")
|
||||
else if (state === "revision_requested") addToast("Revision requested — agent will revise and resubmit", "warn")
|
||||
} catch (e) {
|
||||
setActionError(describeActionError(e))
|
||||
} finally {
|
||||
@@ -80,15 +85,15 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6">
|
||||
<ConfirmDialog open={confirmDelete} title={t("exec.deleteApproval")} message={t("exec.deleteApprovalConfirm")} errorMessage={actionError} onConfirm={() => void handleDelete()} onCancel={() => { setActionError(null); setConfirmDelete(false) }} />
|
||||
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
|
||||
<div className="border-y border-[color:var(--line)] py-4 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`rounded-full px-3 py-1 text-[11px] font-bold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
|
||||
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
|
||||
<StatusPill status={approval.state} />
|
||||
</div>
|
||||
<div className="text-[14px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
|
||||
<div className="text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
|
||||
{approval.impactSummary ? <div className="text-[13px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null}
|
||||
<div className="grid gap-2 text-[13px] md:grid-cols-2">
|
||||
<div className="flex items-center gap-2 text-[color:var(--muted)]"><Robot size={14} />Requested by: <span className="font-medium text-[color:var(--text)]">{requestingAgent?.name ?? "Unknown"}</span>{requestingAgent?.role ? <span className="text-[10px] text-[color:var(--muted)]">({requestingAgent.role})</span> : null}</div>
|
||||
@@ -97,7 +102,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
{approval.updatedAt !== approval.createdAt ? <div className="text-[color:var(--muted)]">Updated: <span className="mono text-[color:var(--text)]">{formatTime(approval.updatedAt)}</span></div> : null}
|
||||
</div>
|
||||
{approval.type === "hire_agent" ? (() => { const payload = parseApprovalPayloadJson(approval); const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null; const reqSnap = payload?.requestedConfigurationSnapshot && typeof payload.requestedConfigurationSnapshot === "object" && !Array.isArray(payload.requestedConfigurationSnapshot) ? payload.requestedConfigurationSnapshot as Record<string, unknown> : null; const proposedAgent = (approval.relatedAgentId ? (agents.find((a) => a.id === approval.relatedAgentId) ?? null) : null) ?? (payloadAgentId ? (agents.find((a) => a.id === payloadAgentId) ?? null) : null); const mgrId = proposedAgent?.reportsTo ?? (typeof reqSnap?.reportsTo === "string" ? reqSnap.reportsTo : null); const mgr = mgrId ? (agents.find((a) => a.id === mgrId) ?? null) : null; if (!proposedAgent && !reqSnap) return null; return (
|
||||
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--accent)]">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
|
||||
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent)]">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
|
||||
<div><span className="text-[color:var(--muted)]">Name:</span> <span className="font-medium text-[color:var(--text)]">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
|
||||
<div><span className="text-[color:var(--muted)]">Role:</span> <span className="text-[color:var(--text)]">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
|
||||
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-[color:var(--muted)]">Title:</span> <span className="text-[color:var(--text)]">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
|
||||
@@ -106,7 +111,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
<div><span className="text-[color:var(--muted)]">Budget:</span> <span className="mono text-[color:var(--text)]">{formatMoney(proposedAgent?.budgetMonthlyUsd ?? (typeof reqSnap?.budgetMonthlyUsd === "number" ? reqSnap.budgetMonthlyUsd : 0))}/mo</span></div>
|
||||
{mgr ? <div><span className="text-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{mgr.name}</span></div> : null}
|
||||
</div></div>) })() : null}
|
||||
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
|
||||
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
|
||||
{isSocialPost && approval.payloadJson ? (
|
||||
<SocialDraftPreview
|
||||
payloadJson={approval.payloadJson}
|
||||
@@ -129,7 +134,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
onClick={() => { setScheduleMode("immediate"); setScheduleError(null) }}
|
||||
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { setScheduleMode("immediate"); setScheduleError(null) } }}
|
||||
>
|
||||
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" /> : null}
|
||||
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">Post immediately</span>
|
||||
</label>
|
||||
@@ -142,7 +147,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
onClick={() => setScheduleMode("scheduled")}
|
||||
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setScheduleMode("scheduled") }}
|
||||
>
|
||||
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" /> : null}
|
||||
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-[color:var(--text)]">Schedule for later</span>
|
||||
</label>
|
||||
@@ -163,7 +168,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold text-[color:var(--muted)]">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
|
||||
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{!isDecided ? <>
|
||||
<ActionButton
|
||||
|
||||
@@ -17,13 +17,13 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
|
||||
overscan: 10,
|
||||
})
|
||||
|
||||
if (approvals.length === 0) return <EmptyState title={t("exec.noApprovals")} />
|
||||
if (approvals.length === 0) return <EmptyState title={t("exec.noApprovals")} detail={t("exec.noApprovalsDetail")} />
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full overflow-auto border-y border-[color:var(--line)]"
|
||||
role="listbox"
|
||||
aria-label={t("exec.noApprovals")}
|
||||
aria-label="Pending approvals"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div
|
||||
@@ -53,13 +53,13 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
|
||||
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
>
|
||||
<button
|
||||
className={`relative w-full border-b border-[color:var(--line)] px-5 py-4 text-left transition last:border-b-0 ${selectedApprovalId === approval.id ? "bg-transparent" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
|
||||
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedApprovalId === approval.id ? "bg-[color:var(--warn-soft)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
|
||||
onClick={() => onSelect(approval.id)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{selectedApprovalId === approval.id ? <span className="absolute inset-y-3 left-0 w-0.5 rounded-full bg-[color:var(--warn)]" /> : null}
|
||||
{selectedApprovalId === approval.id ? <span className="absolute inset-y-0 left-0 w-[3px] bg-[color:var(--warn)]" /> : null}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className={`text-[15px] text-[color:var(--text)] ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
|
||||
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
|
||||
<StatusPill status={approval.state} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
|
||||
overscan: 10,
|
||||
})
|
||||
|
||||
if (costs.length === 0) return <EmptyState title={t("exec.noCostData")} />
|
||||
if (costs.length === 0) return <EmptyState title={t("exec.noCostData")} detail={t("exec.noCostDataDetail")} />
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -66,11 +66,11 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-[14px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div>
|
||||
<div className="mono text-[14px] font-semibold text-[color:var(--text)]">{formatMoney(total)}</div>
|
||||
<div className="text-[13px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div>
|
||||
<div className="mono tabular-nums text-[13px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{formatMoney(total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -91,15 +91,15 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] text-[color:var(--text)]">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
|
||||
<div className="text-[12px] text-[color:var(--muted)]">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] text-[color:var(--text)]">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
|
||||
<div className="text-[11px] text-[color:var(--muted)]">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="mono text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div>
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="mono tabular-nums text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div>
|
||||
{entry.unattributed ? <div className="text-[11px] text-[color:var(--warn)]">unattributed</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,18 +6,18 @@ import { EmptyState, ListFrame } from "../ui"
|
||||
export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentRecord[] }) {
|
||||
const t = useT()
|
||||
const reviewTasks = tasks.filter((tk) => tk.status === "in_review")
|
||||
if (reviewTasks.length === 0) return <EmptyState title={t("exec.noReviewsInProgress")} />
|
||||
if (reviewTasks.length === 0) return <EmptyState title={t("exec.noReviewsInProgress")} detail={t("exec.noReviewsDetail")} />
|
||||
return (
|
||||
<ListFrame>
|
||||
{reviewTasks.map((task) => {
|
||||
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null
|
||||
const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
|
||||
return (
|
||||
<div key={task.id} className="flex items-start gap-3 border-b border-[color:var(--line)] px-5 py-4 last:border-b-0">
|
||||
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-blue-500" />
|
||||
<div key={task.id} className="flex items-start gap-3 border-b border-[color:var(--line)] px-4 py-3 last:border-b-0">
|
||||
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] font-medium text-[color:var(--text)] truncate">{task.title}</div>
|
||||
<div className="mt-0.5 text-[12px] text-[color:var(--muted)]">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> → reviewed by <span className="text-[color:var(--muted-strong)]">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> → reviewed by <span className="text-[color:var(--muted-strong)]">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,38 +8,39 @@ import { formatTime } from "../../lib/formatters"
|
||||
import { unwrap } from "../../lib/desktop"
|
||||
import { ActionButton, ConfirmDialog, EmptyState, InlineNotice, Select } from "../ui"
|
||||
import { describeActionError } from "./execution-utils"
|
||||
import { useAppStore } from "../../state/store"
|
||||
|
||||
export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapshot: ProfileSnapshot; secrets: SecretRecord[]; companyId: string; onSaved: () => Promise<void> }) {
|
||||
const t = useT(); const currentLocale = useLocale()
|
||||
const t = useT(); const currentLocale = useLocale(); const addToast = useAppStore((s) => s.addToast)
|
||||
const [theme, setTheme] = useState(snapshot.theme); const [selectedLocale, setSelectedLocale] = useState<AppLocale>(snapshot.locale ?? currentLocale); const [updatesEnabled, setUpdatesEnabled] = useState(snapshot.updatesEnabled)
|
||||
const [confirmDeleteSecretId, setConfirmDeleteSecretId] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null); const [actionNotice, setActionNotice] = useState<string | null>(null)
|
||||
const [deleteSecretError, setDeleteSecretError] = useState<string | null>(null); const [loadingAction, setLoadingAction] = useState<"save"|"check"|"backup"|"restore"|null>(null)
|
||||
const secretToDelete = secrets.find((s) => s.id === confirmDeleteSecretId) ?? null
|
||||
|
||||
useEffect(() => { setTheme(snapshot.theme); setUpdatesEnabled(snapshot.updatesEnabled); if (snapshot.locale) setSelectedLocale(snapshot.locale) }, [snapshot.theme, snapshot.updatesEnabled, snapshot.locale])
|
||||
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice("Secret deleted."); await onSaved() } catch (e) { setDeleteSecretError(describeActionError(e)) } }
|
||||
async function handleSaveSettings() { setActionError(null); setActionNotice(null); setLoadingAction("save"); try { unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled })); await onSaved(); setActionNotice("Settings saved.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice("Secret deleted."); addToast("Secret deleted", "success"); await onSaved() } catch (e) { setDeleteSecretError(describeActionError(e)) } }
|
||||
async function handleSaveSettings() { setActionError(null); setActionNotice(null); setLoadingAction("save"); try { unwrap(await window.agentCompany.updateSettings({ theme, locale: selectedLocale, updatesEnabled })); await onSaved(); setActionNotice("Settings saved."); addToast("Settings saved", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleCheckForUpdates() { setActionError(null); setActionNotice(null); setLoadingAction("check"); try { const started = unwrap(await window.agentCompany.checkForUpdates()); setActionNotice(started ? "Update check started." : "Update check skipped because no release update feed is configured.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleBackupProfile() { setActionError(null); setActionNotice(null); setLoadingAction("backup"); try { const backupPath = unwrap(await window.agentCompany.backupProfile()); if (backupPath) setActionNotice(`Profile backup created at ${backupPath}.`) } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleRestoreProfile() { setActionError(null); setActionNotice(null); setLoadingAction("restore"); try { const restored = unwrap(await window.agentCompany.restoreProfile()); if (!restored) return; await onSaved(); setActionNotice("Profile restored.") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleBackupProfile() { setActionError(null); setActionNotice(null); setLoadingAction("backup"); try { const backupPath = unwrap(await window.agentCompany.backupProfile()); if (backupPath) { setActionNotice(`Profile backup created at ${backupPath}.`); addToast("Profile backup created", "success") } } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
async function handleRestoreProfile() { setActionError(null); setActionNotice(null); setLoadingAction("restore"); try { const restored = unwrap(await window.agentCompany.restoreProfile()); if (!restored) return; await onSaved(); setActionNotice("Profile restored."); addToast("Profile restored", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setLoadingAction(null) } }
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ConfirmDialog open={confirmDeleteSecretId !== null} title="Delete secret" message={`Permanently delete secret "${secretToDelete?.name ?? ""}"? Any connectors referencing it will lose access.`} errorMessage={deleteSecretError} onConfirm={() => confirmDeleteSecretId && void handleDeleteSecret(confirmDeleteSecretId)} onCancel={() => { setDeleteSecretError(null); setConfirmDeleteSecretId(null) }} />
|
||||
{actionError ? <InlineNotice message={actionError} /> : null}
|
||||
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.profile")}</div><div className="border-y border-[color:var(--line)] py-4"><div className="mb-2 text-[12px] text-[color:var(--muted)]">{t("exec.profilePath")}</div><div className="mono text-[12px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div></div></div>
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
|
||||
<Select label={t("settings.theme")} value={theme} onChange={(v) => setTheme(v as ProfileSnapshot["theme"])} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.profile")}</div><div className="border-y border-[color:var(--line)] py-4"><div className="mb-2 text-[11px] text-[color:var(--muted)]">{t("exec.profilePath")}</div><div className="mono truncate text-[11px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div></div></div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
|
||||
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.dataset.theme = next; root.style.colorScheme = next === "system" ? "light dark" : next; void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved()) }} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
|
||||
<div className="mt-3"><Select label={t("settings.language")} value={selectedLocale} onChange={(v) => setSelectedLocale(v as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} /></div>
|
||||
<div className="border-y border-[color:var(--line)] py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-[color:var(--muted-strong)]"><span>{t("exec.enableUpdater")}</span><input type="checkbox" checked={updatesEnabled} onChange={(e) => setUpdatesEnabled(e.target.checked)} /></label></div>
|
||||
</div>
|
||||
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; return (
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.autoApproveDesc")}</div></div></label></div></div>) })()}
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
|
||||
<div><div className="mb-3 text-[12px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { unwrap } from "../../lib/desktop"
|
||||
import { ActionButton, ConfirmDialog, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
|
||||
import { describeActionError, PriorityDot } from "./execution-utils"
|
||||
import { TaskComments } from "./TaskComments"
|
||||
import { useAppStore } from "../../state/store"
|
||||
|
||||
export function TaskInspector({ task, tasks, runs, agents, goals, projects, workspaces, companyId, onRun, onCancelRun, onCreateApproval, onOpenDiscussion, comments, onRefresh, onDeleted }: {
|
||||
task: TaskRecord; tasks: TaskRecord[]; runs: RunRecord[]; agents: ProfileSnapshot["agents"]; goals: GoalRecord[]; projects: ProjectRecord[]; workspaces: WorkspaceRecord[]; companyId: string
|
||||
@@ -14,32 +15,34 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
comments: CommentRecord[]; onRefresh: () => Promise<void>; onDeleted: () => Promise<void>
|
||||
}) {
|
||||
const t = useT()
|
||||
const addToast = useAppStore((s) => s.addToast)
|
||||
const [editing, setEditing] = useState(false); const [editTitle, setEditTitle] = useState(task.title); const [editDescription, setEditDescription] = useState(task.description)
|
||||
const [editPriority, setEditPriority] = useState(task.priority); const [editStatus, setEditStatus] = useState<TaskStatus>(task.status); const [editAssignee, setEditAssignee] = useState(task.assigneeAgentId ?? "")
|
||||
const [editWorkspace, setEditWorkspace] = useState(task.workspaceId ?? ""); const [editProject, setEditProject] = useState(task.projectId ?? ""); const [editGoal, setEditGoal] = useState(task.goalId ?? "")
|
||||
const [confirmDelete, setConfirmDelete] = useState(false); const [actionError, setActionError] = useState<string | null>(null); const [actionNotice, setActionNotice] = useState<string | null>(null)
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null); const [savingChanges, setSavingChanges] = useState(false); const [updatingStatus, setUpdatingStatus] = useState<TaskStatus | null>(null); const [creatingApproval, setCreatingApproval] = useState(false)
|
||||
|
||||
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null; const project = projects.find((e) => e.id === task.projectId) ?? null
|
||||
const agentMap = useMemo(() => new Map(agents.map((a) => [a.id, a])), [agents]);
|
||||
const assignee = agentMap.get(task.assigneeAgentId ?? "") ?? null; const project = projects.find((e) => e.id === task.projectId) ?? null
|
||||
const goal = goals.find((e) => e.id === task.goalId) ?? null; const workspace = workspaces.find((e) => e.id === task.workspaceId) ?? null
|
||||
const activeRun = runs.find((r) => r.id === task.activeRunId) ?? null; const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
|
||||
const activeRun = runs.find((r) => r.id === task.activeRunId) ?? null; const reviewer = assignee?.reportsTo ? (agentMap.get(assignee.reportsTo) ?? null) : null
|
||||
const needsGovernance = task.status === "blocked" || (task.status === "in_review" && !reviewer)
|
||||
const budgetWarning = assignee && assignee.budgetMonthlyUsd > 0 ? assignee.spentMonthlyUsd / assignee.budgetMonthlyUsd : 0
|
||||
|
||||
function startEdit() { setActionError(null); setActionNotice(null); setEditTitle(task.title); setEditDescription(task.description); setEditPriority(task.priority); setEditStatus(task.status); setEditAssignee(task.assigneeAgentId ?? ""); setEditWorkspace(task.workspaceId ?? ""); setEditProject(task.projectId ?? ""); setEditGoal(task.goalId ?? ""); setEditing(true) }
|
||||
async function saveChanges() { setActionError(null); setActionNotice(null); setSavingChanges(true); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: editTitle, description: editDescription, priority: editPriority as TaskRecord["priority"], status: editStatus, assigneeAgentId: editAssignee || null, workspaceId: editWorkspace || null, projectId: editProject || null, goalId: editGoal || null })); await onRefresh(); setEditing(false); setActionNotice("Task changes saved.") } catch (e) { setActionError(describeActionError(e)) } finally { setSavingChanges(false) } }
|
||||
async function saveChanges() { setActionError(null); setActionNotice(null); setSavingChanges(true); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: editTitle, description: editDescription, priority: editPriority as TaskRecord["priority"], status: editStatus, assigneeAgentId: editAssignee || null, workspaceId: editWorkspace || null, projectId: editProject || null, goalId: editGoal || null })); await onRefresh(); setEditing(false); setActionNotice("Task changes saved."); addToast("Task changes saved", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setSavingChanges(false) } }
|
||||
async function quickStatus(status: TaskStatus) { setActionError(null); setActionNotice(null); setUpdatingStatus(status); try { unwrap(await window.agentCompany.saveTask({ id: task.id, companyId: task.companyId, title: task.title, description: task.description, priority: task.priority, status, assigneeAgentId: task.assigneeAgentId, workspaceId: task.workspaceId, projectId: task.projectId, goalId: task.goalId })); await onRefresh(); setActionNotice(`Task moved to ${status.replaceAll("_", " ")}.`) } catch (e) { setActionError(describeActionError(e)) } finally { setUpdatingStatus(null) } }
|
||||
async function handleCreateApproval() { setActionError(null); setActionNotice(null); setCreatingApproval(true); try { await onCreateApproval(); setActionNotice(task.status === "blocked" ? "Approval created and routed to leadership." : "Approval created.") } catch (e) { setActionError(describeActionError(e)) } finally { setCreatingApproval(false) } }
|
||||
async function handleDelete() { setDeleteError(null); try { unwrap(await window.agentCompany.deleteTask({ id: task.id, companyId })); setConfirmDelete(false); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
|
||||
async function handleCreateApproval() { setActionError(null); setActionNotice(null); setCreatingApproval(true); try { await onCreateApproval(); setActionNotice(task.status === "blocked" ? "Approval created and routed to leadership." : "Approval created."); addToast("Approval created and routed to leadership", "success") } catch (e) { setActionError(describeActionError(e)) } finally { setCreatingApproval(false) } }
|
||||
async function handleDelete() { setDeleteError(null); try { unwrap(await window.agentCompany.deleteTask({ id: task.id, companyId })); setConfirmDelete(false); addToast("Task deleted", "success"); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6">
|
||||
<ConfirmDialog open={confirmDelete} title={t("exec.deleteTask")} message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`} errorMessage={deleteError} onConfirm={() => void handleDelete()} onCancel={() => { setDeleteError(null); setConfirmDelete(false) }} />
|
||||
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-[8px] border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"}`}><Warning size={16} weight="fill" />{assignee.name} has used {Math.round(budgetWarning * 100)}% of their monthly budget ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
|
||||
{actionError ? <InlineNotice message={actionError} /> : null}
|
||||
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
|
||||
{editing ? (
|
||||
<div className="grid gap-3 border-y border-[color:var(--line)] py-4 md:grid-cols-2">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="md:col-span-2"><Input label={t("exec.title")} value={editTitle} onChange={setEditTitle} placeholder={t("exec.taskTitle")} /></div>
|
||||
<div className="md:col-span-2"><TextArea label={t("exec.description")} value={editDescription} onChange={setEditDescription} placeholder={t("exec.description")} rows={4} /></div>
|
||||
<Select label={t("exec.priority")} value={editPriority} onChange={v => setEditPriority(v as TaskRecord["priority"])} options={["critical","high","medium","low"].map(v => ({ value: v, label: v }))} />
|
||||
@@ -52,18 +55,18 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-x-6 gap-y-1 text-[13px] md:grid-cols-2">
|
||||
<div className="grid gap-x-6 gap-y-2 text-[13px] md:grid-cols-2">
|
||||
{assignee && <div className="text-[color:var(--muted)]">Assignee: <span className="text-[color:var(--text)]">{assignee.name}</span></div>}
|
||||
{project && <div className="text-[color:var(--muted)]">Project: <span className="text-[color:var(--text)]">{project.name}</span></div>}
|
||||
{goal && <div className="text-[color:var(--muted)]">Goal: <span className="text-[color:var(--text)]">{goal.title}</span></div>}
|
||||
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-[color:var(--muted)]">Parent: <span className="text-[color:var(--text)]">{parent.title}</span></div> : null })()}
|
||||
{workspace && <div className="text-[color:var(--muted)]">Workspace: <span className="text-[color:var(--text)]">{workspace.name}</span></div>}
|
||||
{task.costUsd > 0 && <div className="text-[color:var(--muted)]">Cost: <span className="mono text-[color:var(--text)]">{formatMoney(task.costUsd)}</span></div>}
|
||||
{task.costUsd > 0 && <div className="text-[color:var(--muted)]">Cost: <span className="mono tabular-nums text-[color:var(--text)]">{formatMoney(task.costUsd)}</span></div>}
|
||||
</div>
|
||||
{task.description ? <div className="text-[13px] leading-relaxed text-[color:var(--muted-strong)] whitespace-pre-wrap">{task.description}</div> : null}
|
||||
{task.status === "in_review" && reviewer ? <div className="rounded-[10px] border border-blue-200 bg-blue-50 px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-blue-700"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-blue-600/80">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
|
||||
: needsGovernance ? <div className="rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
{task.status === "in_review" && reviewer ? <div className="rounded-[8px] border border-[color:var(--accent-soft)] bg-[color:var(--accent-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--accent-text)]"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--accent-text)]">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
|
||||
: needsGovernance ? <div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
|
||||
<div className="flex items-center gap-3">
|
||||
{activeRun ? <ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" /> : <>
|
||||
{["todo","in_progress","blocked"].includes(task.status) && <ActionButton label={t("exec.run")} onClick={() => void onRun(task.id)} icon={Play} tone="accent" />}
|
||||
{task.status === "in_review" && <ActionButton label={t("exec.done")} onClick={() => void quickStatus("done")} icon={CheckCircle} tone="accent" loading={updatingStatus === "done"} />}
|
||||
@@ -79,17 +82,17 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2"><ListChecks size={14} className="text-[color:var(--muted)]" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-[color:var(--success)]">{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done</span></div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]"><div className="h-full rounded-full bg-[color:var(--success)] transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
|
||||
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agents.find((a) => a.id === sub.assigneeAgentId); return (
|
||||
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agentMap.get(sub.assigneeAgentId ?? ""); return (
|
||||
<div key={sub.id} className="flex items-center gap-2 rounded-[6px] px-3 py-2 text-[12px] bg-[color:var(--panel-soft)]">
|
||||
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${sub.status === "done" ? "bg-[color:var(--success)]" : sub.status === "in_progress" ? "bg-[color:var(--accent)]" : sub.status === "blocked" ? "bg-[color:var(--danger)]" : sub.status === "in_review" ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)]"}`} />
|
||||
<span className={`flex-1 truncate ${sub.status === "done" ? "line-through text-[color:var(--muted)]" : "text-[color:var(--text)]"}`}>{sub.title}</span>
|
||||
{subAssignee ? <span className="shrink-0 text-[10px] text-[color:var(--muted)]">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
|
||||
</div>) })}</div></div>) })()}
|
||||
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} />
|
||||
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agents.find((a) => a.id === run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
|
||||
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
|
||||
<div key={run.id} className={`rounded-[8px] border px-3 py-2.5 ${isActive ? "border-[color:var(--success)] bg-[color:var(--success-soft)]" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--danger)] border-opacity-30 bg-[color:var(--danger-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)]"}`}>
|
||||
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="mono">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
|
||||
{run.errorMessage ? <div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)]">{run.errorMessage}</div> : null}
|
||||
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
|
||||
{run.errorMessage ? <div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)] break-words">{run.errorMessage}</div> : null}
|
||||
</div>) })}{runs.length > 8 && <div className="text-[11px] text-[color:var(--muted)] text-center">+{runs.length - 8} more runs</div>}</div></div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,13 +18,13 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
|
||||
overscan: 10,
|
||||
})
|
||||
|
||||
if (tasks.length === 0) return <EmptyState title={t("exec.taskQueueEmpty")} />
|
||||
if (tasks.length === 0) return <EmptyState title={t("exec.taskQueueEmpty")} detail={t("exec.taskQueueEmptyDetail")} />
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full overflow-auto border-y border-[color:var(--line)]"
|
||||
role="listbox"
|
||||
aria-label={t("exec.taskQueueEmpty")}
|
||||
aria-label="Task queue"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div
|
||||
@@ -55,7 +55,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
|
||||
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
|
||||
>
|
||||
<button
|
||||
className={`relative w-full border-b border-[color:var(--line)] px-5 py-4 text-left transition last:border-b-0 ${selectedTaskId === task.id ? "bg-transparent" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
|
||||
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedTaskId === task.id ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
|
||||
onClick={() => onSelect(task.id)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
@@ -63,7 +63,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<PriorityDot priority={task.priority} />
|
||||
<div className={`truncate text-[14px] text-[color:var(--text)] ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
|
||||
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
|
||||
</div>
|
||||
<StatusPill status={activeRun?.status ?? task.status} />
|
||||
</div>
|
||||
|
||||
@@ -15,18 +15,18 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents,
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
|
||||
<div className="flex items-start justify-between gap-3"><div className="min-w-0"><div className="flex items-center gap-2"><div className="text-[15px] font-semibold text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div><div className="mono mt-1 text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div><div className="mt-2 text-[12px] text-[color:var(--muted)]">{project ? <>Project: <span className="text-[color:var(--text)]">{project.name}</span></> : "No linked project"}{workspace.repoUrl ? <span> · Repo: <span className="text-[color:var(--text)]">{workspace.repoUrl}</span></span> : null}{workspace.repoRef ? <span> · Ref: <span className="text-[color:var(--text)]">{workspace.repoRef}</span></span> : null}</div></div><ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} /></div>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{[{ label: t("exec.agents"), val: wAgents.length }, { label: t("exec.tasks"), val: wTasks.length }, { label: t("exec.runs"), val: wRuns.length }, { label: t("exec.docs"), val: wDocs.length }].map((s) => (
|
||||
<div key={s.label} className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{s.val}</div></div>
|
||||
<div key={s.label} className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{s.val}</div></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div><div className="text-[12px] font-medium text-[color:var(--text)]">{a.name}</div><div className="text-[11px] text-[color:var(--muted)]">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-[color:var(--text)]">{tk.title}</span></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-[color:var(--text)]">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div><div className="text-[12px] font-medium text-[color:var(--text)]">{a.name}</div><div className="text-[11px] text-[color:var(--muted)]">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-[color:var(--text)]">{tk.title}</span></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-[color:var(--text)]">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
|
||||
const toDelete = workspaces.find((w) => w.id === confirmDeleteId) ?? null
|
||||
async function handleDelete(id: string) { setDeleteError(null); try { unwrap(await window.agentCompany.deleteWorkspace({ id, companyId })); setConfirmDeleteId(null); await onDeleted() } catch (e) { setDeleteError(describeActionError(e)) } }
|
||||
|
||||
if (workspaces.length === 0) return <EmptyState title={t("exec.noWorkspacesYet")} />
|
||||
if (workspaces.length === 0) return <EmptyState title={t("exec.noWorkspacesYet")} detail={t("exec.noWorkspacesDetail")} />
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog open={confirmDeleteId !== null} title={t("exec.deleteWorkspace")} message={t("exec.deleteWorkspaceConfirm").replace("{0}", toDelete?.name ?? "")} errorMessage={deleteError} onConfirm={() => confirmDeleteId && void handleDelete(confirmDeleteId)} onCancel={() => { setDeleteError(null); setConfirmDeleteId(null) }} />
|
||||
@@ -27,8 +27,8 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
|
||||
<div className={`mb-2 rounded-[8px] p-1 ${selectedWorkspaceId === workspace.id ? "bg-[color:var(--accent-soft)]" : ""}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2"><div className="text-[14px] text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
|
||||
<div className="mono text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div>
|
||||
<div className="flex items-center gap-2"><div className="truncate text-[13px] text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
|
||||
<div className="mono truncate text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workspace.isPrimary ? <StatusPill status="ready" /> : null}
|
||||
@@ -37,7 +37,7 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
|
||||
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[12px] text-[color:var(--muted)]">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-[color:var(--text)]">{linkedProject.name}</span></span> : null}</div>
|
||||
<div className="truncate text-[11px] text-[color:var(--muted)]">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-[color:var(--text)]">{linkedProject.name}</span></span> : null}</div>
|
||||
</div>
|
||||
</ListRow>
|
||||
)
|
||||
|
||||
@@ -58,14 +58,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
|
||||
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("inbox.title")}</h2>
|
||||
<h2 className="text-[18px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{t("inbox.title")}</h2>
|
||||
<p className="text-[12px] text-[color:var(--muted)]">
|
||||
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
|
||||
</p>
|
||||
@@ -81,14 +81,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
|
||||
onClick={() => setFilter(btn.key)}
|
||||
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
|
||||
filter === btn.key
|
||||
? "bg-[color:var(--accent)] text-white"
|
||||
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
|
||||
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"
|
||||
}`}
|
||||
>
|
||||
{btn.label}
|
||||
{btn.count > 0 ? (
|
||||
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${
|
||||
filter === btn.key ? "bg-white/20 text-white" : "bg-[color:var(--line)] text-[color:var(--muted)]"
|
||||
filter === btn.key ? "bg-[color:var(--text-on-accent)]/20 text-[color:var(--text-on-accent)]" : "bg-[color:var(--line)] text-[color:var(--muted)]"
|
||||
}`}>
|
||||
{btn.count}
|
||||
</span>
|
||||
@@ -98,7 +98,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
|
||||
</div>
|
||||
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
|
||||
@@ -115,7 +115,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm ${severityColors[item.severity] ?? ""}`}
|
||||
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px ${severityColors[item.severity] ?? ""}`}
|
||||
>
|
||||
<div
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]"
|
||||
@@ -144,11 +144,11 @@ export function Inbox({ items, onNavigate }: InboxProps) {
|
||||
{item.agentName}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{item.subtitle.slice(0, 80)}</span>
|
||||
<span className="shrink-0">{timeAgo(item.createdAt)}</span>
|
||||
<span className="truncate">{item.subtitle.slice(0, 80)}</span>
|
||||
<span className="shrink-0 tabular-nums">{timeAgo(item.createdAt)}</span>
|
||||
</div>
|
||||
</button>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100" />
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100 group-focus-within:opacity-100" />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
|
||||
const t = useT();
|
||||
if (!metrics) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
|
||||
@@ -48,7 +48,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex-1">
|
||||
@@ -73,7 +73,7 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
|
||||
|
||||
<section>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div>
|
||||
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)]">
|
||||
<div className="overflow-x-auto rounded-[8px] border border-[color:var(--line)]">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]">
|
||||
|
||||
@@ -18,7 +18,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
|
||||
const t = useT();
|
||||
if (!report) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
|
||||
@@ -28,9 +28,9 @@ export function StandupView({ report, onNavigate }: StandupProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
|
||||
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -43,22 +43,22 @@ export function StandupView({ report, onNavigate }: StandupProps) {
|
||||
<div className="text-[14px] font-medium text-[color:var(--text)]">{report.summary}</div>
|
||||
<div className="mt-3 flex gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-[18px] font-bold text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
|
||||
<div className="text-[18px] font-bold tabular-nums text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
|
||||
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
|
||||
</div>
|
||||
<div className="h-10 w-px bg-[color:var(--line)]" />
|
||||
<div className="text-center">
|
||||
<div className="text-[18px] font-bold text-[color:var(--danger)]">{report.blockers.length}</div>
|
||||
<div className="text-[18px] font-bold tabular-nums text-[color:var(--danger)]">{report.blockers.length}</div>
|
||||
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
|
||||
</div>
|
||||
<div className="h-10 w-px bg-[color:var(--line)]" />
|
||||
<div className="text-center">
|
||||
<div className="text-[18px] font-bold text-[color:var(--warn)]">{report.pendingApprovals}</div>
|
||||
<div className="text-[18px] font-bold tabular-nums text-[color:var(--warn)]">{report.pendingApprovals}</div>
|
||||
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
|
||||
</div>
|
||||
<div className="h-10 w-px bg-[color:var(--line)]" />
|
||||
<div className="text-center">
|
||||
<div className={`text-[18px] font-bold ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
|
||||
<div className={`text-[18px] font-bold tabular-nums ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
|
||||
{report.budgetUtilization}%
|
||||
</div>
|
||||
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
|
||||
@@ -114,7 +114,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
|
||||
</div>
|
||||
{ar.status === "running" ? (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-[color:var(--success)]" />
|
||||
</span>
|
||||
) : null}
|
||||
@@ -128,7 +128,7 @@ export function StandupView({ report, onNavigate }: StandupProps) {
|
||||
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null}
|
||||
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</span> : null}
|
||||
{ar.tasksBlocked > 0 ? <span className="text-[color:var(--danger)]">{ar.tasksBlocked} blocked</span> : null}
|
||||
{ar.costLast24h > 0 ? <span>${ar.costLast24h.toFixed(2)}</span> : null}
|
||||
{ar.costLast24h > 0 ? <span className="tabular-nums">${ar.costLast24h.toFixed(2)}</span> : null}
|
||||
</div>
|
||||
{ar.highlights.length > 0 ? (
|
||||
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{ar.highlights.join(" \u00B7 ")}</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function CompactMetric({ label, value, tone = "default", subtitle }: {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className={`text-[22px] font-bold ${toneMap[tone]}`}>{value}</div>
|
||||
{subtitle ? <div className="text-[10px] text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function AgentRoster({
|
||||
})
|
||||
|
||||
if (agents.length === 0) {
|
||||
return <EmptyState title={t("org.noAgentsYet")} />
|
||||
return <EmptyState title={t("org.noAgentsYet")} detail={t("org.noAgentsDetail")} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -79,7 +79,7 @@ export function AgentRoster({
|
||||
type="button"
|
||||
onClick={() => onSelect(agent.id)}
|
||||
tabIndex={-1}
|
||||
className={`block w-full border-b border-[color:var(--line)] px-5 py-4 text-left last:border-b-0 cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
|
||||
className={`block w-full border-b border-[color:var(--line)] px-4 py-3 text-left last:border-b-0 cursor-pointer transition-colors duration-150 ${isSelected ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "hover:bg-[color:var(--panel-soft)]"}`}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -87,15 +87,15 @@ export function AgentRoster({
|
||||
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-[15px] text-[color:var(--text)]">{agent.name}</div>
|
||||
<div className="text-[12px] text-[color:var(--muted)]">
|
||||
<div className={`truncate text-[14px] font-semibold text-[color:var(--text)]`}>{agent.name}</div>
|
||||
<div className="truncate text-[11px] text-[color:var(--muted)]">
|
||||
{agent.role} · {agent.title || "No title"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatusPill status={agent.status} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[12px] text-[color:var(--muted)]">
|
||||
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
|
||||
<span>Connector: {connector?.label ?? agent.connectorId}</span>
|
||||
{workspace && <span>Workspace: {workspace.name}</span>}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function GoalList({
|
||||
}) {
|
||||
const t = useT();
|
||||
if (goals.length === 0) {
|
||||
return <EmptyState title={t("org.noGoalsYet")} />;
|
||||
return <EmptyState title={t("org.noGoalsYet")} detail={t("org.noGoalsDetail")} />;
|
||||
}
|
||||
return (
|
||||
<ListFrame>
|
||||
@@ -37,12 +37,12 @@ export function GoalList({
|
||||
{isSelected && (
|
||||
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
|
||||
)}
|
||||
<div className="text-[15px] text-[color:var(--text)]">{goal.title}</div>
|
||||
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{goal.title}</div>
|
||||
</div>
|
||||
<StatusPill status={goal.status} />
|
||||
</div>
|
||||
<div className="text-[13px] text-[color:var(--muted)]">{goal.description}</div>
|
||||
<div className="mt-1 flex gap-4 text-[12px] text-[color:var(--muted)]">
|
||||
<div className="line-clamp-2 text-[13px] text-[color:var(--muted)]">{goal.description}</div>
|
||||
<div className="mt-1 flex gap-4 text-[11px] text-[color:var(--muted)]">
|
||||
{parentGoal && <span>Parent: {parentGoal.title}</span>}
|
||||
{ownerAgent && <span>Owner: {ownerAgent.name}</span>}
|
||||
</div>
|
||||
|
||||
@@ -67,13 +67,13 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
|
||||
setZoom(nz);
|
||||
}, [zoom, pan]);
|
||||
|
||||
if (agents.length === 0) return <EmptyState title={t("org.noAgentsToDisplay")} />;
|
||||
if (agents.length === 0) return <EmptyState title={t("org.noAgentsToDisplay")} detail={t("org.noAgentsToDisplayDetail")} />;
|
||||
|
||||
const zoomIn = () => { const nz = Math.min(zoom * 1.2, 2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
|
||||
const zoomOut = () => { const nz = Math.max(zoom * 0.8, 0.2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative h-[480px] w-full overflow-hidden rounded-[10px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
|
||||
<div ref={containerRef} className="relative min-h-[400px] h-[clamp(400px,60vh,720px)] w-full overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
|
||||
<div className="absolute right-3 top-3 z-10 flex flex-col gap-1">
|
||||
{[{ label: "+", ariaLabel: "Zoom in", action: zoomIn }, { label: "\u2212", ariaLabel: "Zoom out", action: zoomOut }, { label: "Fit", ariaLabel: "Fit chart to screen", action: fitToScreen }].map((btn) => (
|
||||
<button key={btn.ariaLabel} className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] text-[12px] text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]" onClick={btn.action} aria-label={btn.ariaLabel}>{btn.label}</button>
|
||||
@@ -100,22 +100,23 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
|
||||
const isPending = node.agent.status === "pending_approval";
|
||||
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
|
||||
return (
|
||||
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[10px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
|
||||
<div className={`h-[3px] rounded-t-[9px] ${isCEO ? "bg-[color:var(--accent)]" : node.children.length > 0 ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)] opacity-30"}`} />
|
||||
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[8px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
|
||||
<div className={`rounded-t-[7px] ${isCEO ? "h-[4px] bg-[color:var(--accent)]" : node.children.length > 0 ? "h-[3px] bg-[color:var(--warn)]" : "h-[2px] bg-[color:var(--muted)] opacity-30"}`} />
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div className="relative shrink-0">
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
|
||||
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
|
||||
{isRunning ? (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor }} />
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5" aria-hidden="true">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor, willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
|
||||
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} aria-hidden="true" />
|
||||
)}
|
||||
<span className="sr-only">{`Status: ${node.agent.status}`}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||
<span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
|
||||
<span className="truncate text-[14px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
|
||||
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">{node.agent.title || node.agent.role}</span>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{connectorLabel}</span>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ProjectList({
|
||||
}) {
|
||||
const t = useT();
|
||||
if (projects.length === 0) {
|
||||
return <EmptyState title={t("org.noProjectsYet")} />;
|
||||
return <EmptyState title={t("org.noProjectsYet")} detail={t("org.noProjectsDetail")} />;
|
||||
}
|
||||
return (
|
||||
<ListFrame>
|
||||
@@ -38,12 +38,12 @@ export function ProjectList({
|
||||
{isSelected && (
|
||||
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
|
||||
)}
|
||||
<div className="text-[15px] text-[color:var(--text)]">{project.name}</div>
|
||||
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{project.name}</div>
|
||||
</div>
|
||||
<StatusPill status={project.status} />
|
||||
</div>
|
||||
<div className="mb-2 text-[13px] text-[color:var(--muted)]">{project.description}</div>
|
||||
<div className="flex flex-wrap gap-4 text-[12px] text-[color:var(--muted)]">
|
||||
<div className="flex flex-wrap gap-4 text-[11px] text-[color:var(--muted)]">
|
||||
<span>
|
||||
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
|
||||
</span>
|
||||
|
||||
@@ -86,12 +86,13 @@ export function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; c
|
||||
}
|
||||
|
||||
export const statusDotColor: Record<string, string> = {
|
||||
running: "#22d3ee",
|
||||
active: "#4ade80",
|
||||
paused: "#facc15",
|
||||
idle: "#facc15",
|
||||
error: "#f87171",
|
||||
terminated: "#a3a3a3",
|
||||
running: "var(--accent)",
|
||||
active: "var(--success)",
|
||||
paused: "var(--warn)",
|
||||
idle: "var(--muted)",
|
||||
error: "var(--danger)",
|
||||
terminated: "var(--muted)",
|
||||
pending_approval: "var(--warn)",
|
||||
};
|
||||
|
||||
export const adapterLabels: Record<string, string> = {
|
||||
|
||||
@@ -82,7 +82,7 @@ export function RunDetail({
|
||||
<DetailRow label="Cost">{formatMoney(selectedRun.costUsd)}</DetailRow>
|
||||
</div>
|
||||
{selectedRun.errorMessage ? (
|
||||
<div className="mt-4 rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className="mt-4 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
{selectedRun.errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -118,14 +118,14 @@ export function RunDetail({
|
||||
)}
|
||||
>
|
||||
{logState.error ? (
|
||||
<div className="rounded-[10px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
<div className="rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
|
||||
{logState.error}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-3 flex items-center justify-between text-[11px] text-[color:var(--muted)]">
|
||||
<span>{logState.loading ? "Refreshing\u2026" : logState.eof ? "At latest tail" : "Tail window loaded"}</span>
|
||||
</div>
|
||||
<div className="max-h-[420px] overflow-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
<div className="max-h-[420px] overflow-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
|
||||
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
|
||||
{logState.content || "No log output captured yet."}
|
||||
</pre>
|
||||
|
||||
@@ -45,7 +45,7 @@ export function RunSummaryCard({
|
||||
tone?: "default" | "danger";
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
|
||||
@@ -54,7 +54,7 @@ export function RunSummaryCard({
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{detail}</div>
|
||||
</div>
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-[10px] ${tone === "danger" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-[8px] ${tone === "danger" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export function RunSummaryCard({
|
||||
|
||||
export function RunMeta({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className="mt-1 text-[13px] text-[color:var(--text)]">{value}</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
|
||||
const cardContent = (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-semibold text-[color:var(--text)]">{account.displayName || account.accountName}</span>
|
||||
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{account.displayName || account.accountName}</span>
|
||||
<span
|
||||
className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
|
||||
style={{ backgroundColor: platform.bgColor, color: platform.color }}
|
||||
@@ -26,7 +26,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
|
||||
<span>@{account.accountName}</span>
|
||||
<span className="truncate">@{account.accountName}</span>
|
||||
{account.requireApproval ? (
|
||||
<span className="flex items-center gap-1 text-[color:var(--warn)]">
|
||||
<ShieldCheck size={10} />
|
||||
@@ -39,9 +39,9 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition hover:border-[color:var(--line-strong)] hover:shadow-sm">
|
||||
<div className="group flex items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px">
|
||||
<div
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[10px]"
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[8px]"
|
||||
style={{ backgroundColor: platform.bgColor, color: platform.color }}
|
||||
>
|
||||
<PlatformIcon size={18} weight="fill" />
|
||||
@@ -78,7 +78,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogin}
|
||||
className="flex items-center gap-1 rounded-[6px] bg-[color:var(--accent)] px-2.5 py-1.5 text-[10px] font-semibold text-white transition hover:opacity-90"
|
||||
className="flex items-center gap-1 rounded-[6px] bg-[color:var(--accent)] px-2.5 py-1.5 text-[10px] font-semibold text-[color:var(--text-on-accent)] transition hover:opacity-90"
|
||||
>
|
||||
<SignIn size={12} />
|
||||
Login
|
||||
@@ -88,7 +88,8 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
className="rounded-[6px] p-1.5 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100"
|
||||
aria-label="Delete account"
|
||||
className="rounded-[6px] p-1.5 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100 group-focus-within:opacity-100"
|
||||
>
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
className="w-full max-w-[420px] rounded-[14px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-xl"
|
||||
className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
|
||||
@@ -52,7 +52,7 @@ export function SocialAccountsPanel({
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-[color:var(--accent-soft)]">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
|
||||
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -70,7 +70,7 @@ export function SocialAccountsPanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[10px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3">
|
||||
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3">
|
||||
<div className="flex items-start gap-2 text-[12px] text-[color:var(--accent)]">
|
||||
<Browser size={16} className="mt-0.5 shrink-0" />
|
||||
<div>
|
||||
@@ -111,7 +111,7 @@ export function SocialAccountsPanel({
|
||||
|
||||
{activeTab === "accounts" ? (
|
||||
accounts.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Megaphone size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, useEffect, useRef, type ElementType, type ErrorInfo, type Re
|
||||
|
||||
export function Badge({ count, tone = "default" }: { count: number; tone?: "default" | "warn" | "danger" }) {
|
||||
if (count <= 0) return null;
|
||||
const toneClass = tone === "danger" ? "bg-[color:var(--danger)] text-white" : tone === "warn" ? "bg-[color:var(--warn)] text-white" : "bg-[color:var(--muted)] text-white";
|
||||
const toneClass = tone === "danger" ? "bg-[color:var(--danger)] text-[color:var(--text-on-accent)]" : tone === "warn" ? "bg-[color:var(--warn)] text-[color:var(--text-on-accent)]" : "bg-[color:var(--muted)] text-[color:var(--text-on-accent)]";
|
||||
return (
|
||||
<span className={`ml-auto inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1.5 text-[10px] font-bold ${toneClass}`}>
|
||||
{count > 99 ? "99+" : count}
|
||||
@@ -52,15 +52,16 @@ export function SectionButton({
|
||||
return (
|
||||
<button
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`focus-ring flex w-full items-center gap-2.5 rounded-[6px] px-3 py-[7px] text-left transition duration-150 ${
|
||||
className={`focus-ring relative flex w-full items-center gap-2.5 rounded-[6px] px-3 py-[7px] text-left transition duration-150 ${
|
||||
active
|
||||
? "bg-[color:var(--panel-soft)] text-[color:var(--text)]"
|
||||
? "bg-[color:var(--accent-soft)] text-[color:var(--text)]"
|
||||
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{active ? <span className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
|
||||
<Icon size={17} weight={active ? "fill" : "regular"} className={active ? "text-[color:var(--accent)]" : ""} />
|
||||
<span className={`text-[13px] tracking-[0.005em] ${active ? "font-semibold" : "font-medium"}`}>{label}</span>
|
||||
<span className={`text-[13px] tracking-[0.005em] ${active ? "font-bold" : "font-medium"}`}>{label}</span>
|
||||
{badge ? <Badge count={badge} tone={badgeTone} /> : null}
|
||||
</button>
|
||||
);
|
||||
@@ -85,8 +86,8 @@ export function Panel({
|
||||
<section className={`space-y-4 ${className}`}>
|
||||
<div className={`flex items-end justify-between gap-4 ${tone === "surface" ? "border-b border-[color:var(--line)] pb-3" : ""}`}>
|
||||
<div>
|
||||
{eyebrow ? <div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{eyebrow}</div> : null}
|
||||
<h2 className="text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">{title}</h2>
|
||||
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)]">{eyebrow}</div> : null}
|
||||
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-[color:var(--text)]">{title}</h2>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
@@ -127,12 +128,12 @@ export function Modal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="flex w-full flex-col rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-xl"
|
||||
className="flex w-full flex-col rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-xl"
|
||||
style={{ maxWidth: width, maxHeight: "80vh" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-6 py-4">
|
||||
<h2 className="text-[16px] font-semibold text-[color:var(--text)]">{title}</h2>
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-5 py-3.5">
|
||||
<h2 className="text-[16px] font-bold tracking-[-0.015em] text-[color:var(--text)]">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="focus-ring rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
|
||||
@@ -142,7 +143,7 @@ export function Modal({
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto px-6 py-5">{children}</div>
|
||||
<div className="overflow-y-auto px-5 py-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -303,14 +304,14 @@ export function ActionButton({
|
||||
const isDisabled = loading || disabled;
|
||||
const toneClass =
|
||||
tone === "accent"
|
||||
? "border-transparent bg-[color:var(--accent)] text-white hover:brightness-90"
|
||||
? "border-transparent bg-[color:var(--accent)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(74,93,122,0.35)] hover:bg-[color:var(--accent-hover)]"
|
||||
: tone === "danger"
|
||||
? "border-transparent bg-[color:var(--danger)] text-white hover:brightness-90"
|
||||
? "border-transparent bg-[color:var(--danger)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(220,53,69,0.3)] hover:brightness-90"
|
||||
: "border-[color:var(--line)] bg-[color:var(--panel)] text-[color:var(--text)] hover:bg-[color:var(--panel-soft)]";
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`focus-ring inline-flex items-center gap-2 rounded-[7px] border px-3.5 py-2 text-[12px] font-semibold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""}`}
|
||||
className={`focus-ring inline-flex items-center gap-2 rounded-[8px] border px-4 py-2.5 text-[12px] font-bold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
|
||||
onClick={onClick}
|
||||
disabled={isDisabled}
|
||||
aria-busy={loading}
|
||||
@@ -351,11 +352,11 @@ export function StatusPill({ status }: { status: string }) {
|
||||
const displayLabel = checking ? "checking" : status.replaceAll("_", " ");
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] ${toneClass}`}>
|
||||
<span className={`inline-flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] ${toneClass}`}>
|
||||
{checking ? (
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--muted)]" aria-hidden="true" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--muted)]" aria-hidden="true" />
|
||||
) : (
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${dotClass}`} aria-hidden="true" />
|
||||
<span className={`h-2 w-2 rounded-full ${dotClass}`} aria-hidden="true" />
|
||||
)}
|
||||
{displayLabel}
|
||||
</span>
|
||||
@@ -365,9 +366,11 @@ export function StatusPill({ status }: { status: string }) {
|
||||
export function InlineNotice({
|
||||
message,
|
||||
tone = "danger",
|
||||
action,
|
||||
}: {
|
||||
message: string;
|
||||
tone?: "danger" | "success" | "warn";
|
||||
action?: ReactNode;
|
||||
}) {
|
||||
const toneClass = tone === "success"
|
||||
? "border-[color:var(--success)] bg-[color:var(--success-soft)] text-[color:var(--success)]"
|
||||
@@ -375,8 +378,13 @@ export function InlineNotice({
|
||||
? "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
|
||||
: "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]";
|
||||
return (
|
||||
<div className={`rounded-[10px] border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
|
||||
{message}
|
||||
<div className={`rounded-[8px] border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
|
||||
{action ? (
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span>{message}</span>
|
||||
{action}
|
||||
</div>
|
||||
) : message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -408,7 +416,7 @@ export function ConfirmDialog({ open, title, message, errorMessage, onConfirm, o
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
className="w-full max-w-[400px] rounded-[12px] bg-[color:var(--panel)] border border-[color:var(--line)] p-6 shadow-lg"
|
||||
className="w-full max-w-[400px] rounded-[8px] bg-[color:var(--panel)] border border-[color:var(--line)] p-6 shadow-lg"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div id="confirm-dialog-title" className="text-[16px] font-semibold text-[color:var(--text)] mb-2">{title}</div>
|
||||
|
||||
+32
-20
@@ -35,7 +35,7 @@ export const en: Record<string, string> = {
|
||||
"nav.group.infrastructure": "Infrastructure",
|
||||
|
||||
// ── App-level strings ──
|
||||
"app.loading": "Loading local profile...",
|
||||
"app.loading": "Setting up your workspace...",
|
||||
"app.selectCompany": "Select company",
|
||||
"app.newCompany": "New company",
|
||||
"app.commandPalette": "Command palette",
|
||||
@@ -107,9 +107,9 @@ export const en: Record<string, string> = {
|
||||
"onboarding.enterCompanyName": "Enter a company name.",
|
||||
"onboarding.connectorRequired": "A ready connector is required to continue.",
|
||||
"onboarding.describeFirstGoal": "Describe the first real company goal.",
|
||||
"onboarding.unableToBootstrap": "Unable to bootstrap the company.",
|
||||
"onboarding.unableToBootstrap": "Unable to launch the company. Check your connector authentication and try again.",
|
||||
"onboarding.unableToOpenControlCenter": "Unable to open the control center.",
|
||||
"onboarding.connectorNotInstalled": "Install and authenticate a CLI runtime first.",
|
||||
"onboarding.connectorNotInstalled": "No CLI runtime detected. Install and authenticate a supported runtime to continue.",
|
||||
"onboarding.connectorWarning": "Install and authenticate a CLI runtime first.",
|
||||
"onboarding.languageSelection": "Language",
|
||||
"onboarding.selectLanguage": "Select language",
|
||||
@@ -289,12 +289,13 @@ export const en: Record<string, string> = {
|
||||
|
||||
// ── Common panel titles ──
|
||||
"panel.choose": "Choose",
|
||||
"panel.noData": "No data",
|
||||
"panel.noData": "Nothing to show yet",
|
||||
"panel.empty": "Empty",
|
||||
"panel.loading": "Loading...",
|
||||
|
||||
// ── Execution / Task Inspector ──
|
||||
"exec.taskQueueEmpty": "Task queue is empty",
|
||||
"exec.taskQueueEmpty": "All caught up",
|
||||
"exec.taskQueueEmptyDetail": "New tasks will appear here as agents plan and break down work.",
|
||||
"exec.deleteTask": "Delete task",
|
||||
"exec.deleteTaskConfirm": "Permanently delete \"{0}\"? This cannot be undone.",
|
||||
"exec.budgetUsed": "has used {0}% of their monthly budget",
|
||||
@@ -348,10 +349,12 @@ export const en: Record<string, string> = {
|
||||
"exec.send": "Send",
|
||||
"exec.createTask": "Create task",
|
||||
"exec.taskTitle": "Task title",
|
||||
"exec.noReviewsInProgress": "No agent reviews in progress",
|
||||
"exec.noReviewsInProgress": "No reviews pending",
|
||||
"exec.noReviewsDetail": "Tasks waiting for agent review will appear here.",
|
||||
"exec.awaitingReview": "awaiting review",
|
||||
"exec.reviewedBy": "reviewed by",
|
||||
"exec.noApprovals": "No approvals",
|
||||
"exec.noApprovals": "No pending approvals",
|
||||
"exec.noApprovalsDetail": "Approval requests from agents will appear here when they need your sign-off.",
|
||||
"exec.deleteApproval": "Delete approval",
|
||||
"exec.deleteApprovalConfirm": "Permanently delete this approval? This cannot be undone.",
|
||||
"exec.requestedBy": "Requested by:",
|
||||
@@ -373,7 +376,8 @@ export const en: Record<string, string> = {
|
||||
"exec.requestRevision": "Request revision",
|
||||
"exec.reject": "Reject",
|
||||
"exec.lastNote": "Last note:",
|
||||
"exec.noCostData": "No cost data yet",
|
||||
"exec.noCostData": "No costs recorded yet",
|
||||
"exec.noCostDataDetail": "API usage and token costs will be tracked here once agents start running.",
|
||||
"exec.allConnectors": "All connectors",
|
||||
"exec.runCost": "Run cost",
|
||||
"exec.unknownAgent": "Unknown agent",
|
||||
@@ -381,19 +385,21 @@ export const en: Record<string, string> = {
|
||||
"exec.totalEntries": "Total ({0} entries)",
|
||||
"exec.knownSpend": "Known spend",
|
||||
"exec.unattributedEntries": "Unattributed entries",
|
||||
"exec.noActivityYet": "No activity yet",
|
||||
"exec.noActivityYet": "No activity recorded yet",
|
||||
"exec.noActivityDetail": "Agent actions, task updates, and system events will stream here.",
|
||||
"exec.searchActivity": "Search activity...",
|
||||
"exec.allTypes": "All types",
|
||||
"exec.noMatchingActivity": "No matching activity",
|
||||
"exec.noMatchingActivity": "No activity matches this filter",
|
||||
"exec.loadMore": "Load more ({0} remaining)",
|
||||
"exec.noWorkspacesYet": "No workspaces yet",
|
||||
"exec.noWorkspacesYet": "No workspaces created yet",
|
||||
"exec.noWorkspacesDetail": "Workspaces are created automatically when your company launches.",
|
||||
"exec.deleteWorkspace": "Delete workspace",
|
||||
"exec.deleteWorkspaceConfirm": "Delete workspace \"{0}\"? This only removes it from the company registry; files on disk are not affected.",
|
||||
"exec.inspect": "Inspect",
|
||||
"exec.reveal": "Reveal",
|
||||
"exec.noRemoteUrl": "No remote URL",
|
||||
"exec.selectWorkspace": "Select a workspace",
|
||||
"exec.selectWorkspaceDetail": "Select a workspace to view its details.",
|
||||
"exec.selectWorkspaceDetail": "Choose a workspace from the list to view agents, tasks, and activity.",
|
||||
"exec.agents": "Agents",
|
||||
"exec.tasks": "Tasks",
|
||||
"exec.runs": "Runs",
|
||||
@@ -525,8 +531,10 @@ export const en: Record<string, string> = {
|
||||
"dash.poor": "Poor",
|
||||
"dash.critical": "Critical",
|
||||
"dash.noAgentsYet": "No agents hired yet",
|
||||
"dash.noAgentsDetail": "Your CEO is working on hiring. Check back shortly.",
|
||||
"dash.createCeoAgent": "",
|
||||
"dash.noActivityYet": "No activity yet",
|
||||
"dash.noActivityYet": "Waiting for activity",
|
||||
"dash.noActivityDetail": "Agent actions and events will stream here in real time.",
|
||||
"dash.liveRuns": "Live Runs",
|
||||
"dash.running": "running",
|
||||
"dash.allQuiet": "All quiet",
|
||||
@@ -577,11 +585,15 @@ export const en: Record<string, string> = {
|
||||
"dash.points": "points",
|
||||
|
||||
// ── Organization ──
|
||||
"org.noAgentsYet": "No agents yet",
|
||||
"org.noAgentsYet": "No agents hired yet",
|
||||
"org.noAgentsDetail": "Agents join your company through the CEO\u2019s hiring decisions or your direct approval.",
|
||||
"org.noTitle": "No title",
|
||||
"org.noGoalsYet": "No goals yet",
|
||||
"org.noProjectsYet": "No projects yet",
|
||||
"org.noAgentsToDisplay": "No agents to display",
|
||||
"org.noGoalsYet": "No goals defined yet",
|
||||
"org.noGoalsDetail": "Goals are set during company creation and drive how agents prioritize work.",
|
||||
"org.noProjectsYet": "No projects created yet",
|
||||
"org.noProjectsDetail": "The CEO organizes work into projects as the company scales.",
|
||||
"org.noAgentsToDisplay": "Organization chart is empty",
|
||||
"org.noAgentsToDisplayDetail": "Agents will appear here once hired, showing reporting structure and status.",
|
||||
"org.saveChanges": "Save changes",
|
||||
"org.submitHireRequest": "Submit hire request",
|
||||
"org.createGoal": "Create goal",
|
||||
@@ -611,8 +623,8 @@ export const en: Record<string, string> = {
|
||||
"org.fit": "Fit",
|
||||
|
||||
// ── Runs ──
|
||||
"runs.noRunsYet": "No runs yet",
|
||||
"runs.noRunsDetail": "Runs appear here once tasks are executed.",
|
||||
"runs.noRunsYet": "No agent runs yet",
|
||||
"runs.noRunsDetail": "Runs are created when agents execute tasks. They\u2019ll appear here with logs and status.",
|
||||
"runs.activeLabel": "Active",
|
||||
"runs.queuedOrRunning": "Queued or running right now",
|
||||
"runs.needsAttention": "Needs attention",
|
||||
@@ -622,7 +634,7 @@ export const en: Record<string, string> = {
|
||||
"runs.runQueue": "Run queue",
|
||||
"runs.searchRuns": "Search by task, agent, connector, workspace, or error",
|
||||
"runs.noRunsMatch": "No runs match this filter",
|
||||
"runs.noRunsMatchDetail": "Try adjusting the filters.",
|
||||
"runs.noRunsMatchDetail": "Try broadening your filter or clearing it to see all runs.",
|
||||
"runs.runDetail": "Run detail",
|
||||
"runs.openConsole": "Open console",
|
||||
"runs.cancelRun": "Cancel run",
|
||||
|
||||
+30
-18
@@ -35,7 +35,7 @@ export const zh: Record<string, string> = {
|
||||
"nav.group.infrastructure": "基础设施",
|
||||
|
||||
// ── 应用级文本 ──
|
||||
"app.loading": "正在加载本地配置...",
|
||||
"app.loading": "正在准备您的工作区...",
|
||||
"app.selectCompany": "选择公司",
|
||||
"app.newCompany": "新建公司",
|
||||
"app.commandPalette": "命令面板",
|
||||
@@ -107,9 +107,9 @@ export const zh: Record<string, string> = {
|
||||
"onboarding.enterCompanyName": "请输入公司名称。",
|
||||
"onboarding.connectorRequired": "需要一个就绪的连接器才能继续。",
|
||||
"onboarding.describeFirstGoal": "请描述公司的第一个目标。",
|
||||
"onboarding.unableToBootstrap": "无法初始化公司。",
|
||||
"onboarding.unableToBootstrap": "无法启动公司。请检查连接器认证后重试。",
|
||||
"onboarding.unableToOpenControlCenter": "无法打开控制中心。",
|
||||
"onboarding.connectorNotInstalled": "请先安装并认证一个 CLI 运行时。",
|
||||
"onboarding.connectorNotInstalled": "未检测到 CLI 运行时。请安装并认证一个受支持的运行时以继续。",
|
||||
"onboarding.connectorWarning": "请先安装并认证一个 CLI 运行时。",
|
||||
"onboarding.languageSelection": "语言",
|
||||
"onboarding.selectLanguage": "选择语言",
|
||||
@@ -289,12 +289,13 @@ export const zh: Record<string, string> = {
|
||||
|
||||
// ── 通用面板标题 ──
|
||||
"panel.choose": "选择",
|
||||
"panel.noData": "暂无数据",
|
||||
"panel.noData": "暂无内容",
|
||||
"panel.empty": "空",
|
||||
"panel.loading": "加载中...",
|
||||
|
||||
// ── 执行/任务检查器 ──
|
||||
"exec.taskQueueEmpty": "任务队列为空",
|
||||
"exec.taskQueueEmpty": "暂无待办任务",
|
||||
"exec.taskQueueEmptyDetail": "智能体规划和分解工作时,新任务将出现在这里。",
|
||||
"exec.deleteTask": "删除任务",
|
||||
"exec.deleteTaskConfirm": "永久删除「{0}」?此操作无法撤消。",
|
||||
"exec.budgetUsed": "已使用 {0}% 的月度预算",
|
||||
@@ -348,10 +349,12 @@ export const zh: Record<string, string> = {
|
||||
"exec.send": "发送",
|
||||
"exec.createTask": "创建任务",
|
||||
"exec.taskTitle": "任务标题",
|
||||
"exec.noReviewsInProgress": "没有进行中的智能体审核",
|
||||
"exec.noReviewsInProgress": "暂无待审核任务",
|
||||
"exec.noReviewsDetail": "等待智能体审核的任务将出现在这里。",
|
||||
"exec.awaitingReview": "等待审核",
|
||||
"exec.reviewedBy": "由...审核",
|
||||
"exec.noApprovals": "暂无审批",
|
||||
"exec.noApprovals": "暂无待审批项",
|
||||
"exec.noApprovalsDetail": "智能体需要您批准时,审批请求将出现在这里。",
|
||||
"exec.deleteApproval": "删除审批",
|
||||
"exec.deleteApprovalConfirm": "永久删除此审批?此操作无法撤消。",
|
||||
"exec.requestedBy": "请求者:",
|
||||
@@ -373,7 +376,8 @@ export const zh: Record<string, string> = {
|
||||
"exec.requestRevision": "要求修改",
|
||||
"exec.reject": "拒绝",
|
||||
"exec.lastNote": "最近备注:",
|
||||
"exec.noCostData": "暂无费用数据",
|
||||
"exec.noCostData": "暂无费用记录",
|
||||
"exec.noCostDataDetail": "智能体开始运行后,API 使用和令牌费用将在此处跟踪。",
|
||||
"exec.allConnectors": "全部连接器",
|
||||
"exec.runCost": "运行费用",
|
||||
"exec.unknownAgent": "未知智能体",
|
||||
@@ -381,19 +385,21 @@ export const zh: Record<string, string> = {
|
||||
"exec.totalEntries": "合计({0} 条记录)",
|
||||
"exec.knownSpend": "已知支出",
|
||||
"exec.unattributedEntries": "未归因条目",
|
||||
"exec.noActivityYet": "暂无活动",
|
||||
"exec.noActivityYet": "暂无活动记录",
|
||||
"exec.noActivityDetail": "智能体操作、任务更新和系统事件将在此处实时显示。",
|
||||
"exec.searchActivity": "搜索活动...",
|
||||
"exec.allTypes": "全部类型",
|
||||
"exec.noMatchingActivity": "无匹配活动",
|
||||
"exec.noMatchingActivity": "没有匹配此筛选条件的活动",
|
||||
"exec.loadMore": "加载更多(还剩 {0} 条)",
|
||||
"exec.noWorkspacesYet": "暂无工作区",
|
||||
"exec.noWorkspacesDetail": "公司启动时将自动创建工作区。",
|
||||
"exec.deleteWorkspace": "删除工作区",
|
||||
"exec.deleteWorkspaceConfirm": "删除工作区「{0}」?这只会从公司注册表中移除,不会影响磁盘上的文件。",
|
||||
"exec.inspect": "检查",
|
||||
"exec.reveal": "在文件管理器中显示",
|
||||
"exec.noRemoteUrl": "无远程地址",
|
||||
"exec.selectWorkspace": "选择一个工作区",
|
||||
"exec.selectWorkspaceDetail": "选择一个工作区以查看详情。",
|
||||
"exec.selectWorkspaceDetail": "从列表中选择一个工作区,查看其智能体、任务和活动。",
|
||||
"exec.agents": "智能体",
|
||||
"exec.tasks": "任务",
|
||||
"exec.runs": "运行",
|
||||
@@ -525,8 +531,10 @@ export const zh: Record<string, string> = {
|
||||
"dash.poor": "较差",
|
||||
"dash.critical": "危急",
|
||||
"dash.noAgentsYet": "尚未招聘智能体",
|
||||
"dash.noAgentsDetail": "您的 CEO 正在招聘中,请稍后查看。",
|
||||
"dash.createCeoAgent": "",
|
||||
"dash.noActivityYet": "暂无活动",
|
||||
"dash.noActivityYet": "等待活动",
|
||||
"dash.noActivityDetail": "智能体的操作和事件将在此实时显示。",
|
||||
"dash.liveRuns": "实时运行",
|
||||
"dash.running": "运行中",
|
||||
"dash.allQuiet": "一切平静",
|
||||
@@ -577,11 +585,15 @@ export const zh: Record<string, string> = {
|
||||
"dash.points": "点数",
|
||||
|
||||
// ── 组织 ──
|
||||
"org.noAgentsYet": "暂无智能体",
|
||||
"org.noAgentsYet": "暂无已招聘的智能体",
|
||||
"org.noAgentsDetail": "智能体将通过 CEO 的招聘决策或您的直接批准加入公司。",
|
||||
"org.noTitle": "无职位",
|
||||
"org.noGoalsYet": "暂无目标",
|
||||
"org.noGoalsYet": "暂未设定目标",
|
||||
"org.noGoalsDetail": "目标在创建公司时设定,驱动智能体的工作优先级。",
|
||||
"org.noProjectsYet": "暂无项目",
|
||||
"org.noAgentsToDisplay": "没有可显示的智能体",
|
||||
"org.noProjectsDetail": "CEO 将在公司扩展时组织项目。",
|
||||
"org.noAgentsToDisplay": "组织架构图为空",
|
||||
"org.noAgentsToDisplayDetail": "智能体招聘后将在此显示,展示汇报结构和状态。",
|
||||
"org.saveChanges": "保存更改",
|
||||
"org.submitHireRequest": "提交招聘请求",
|
||||
"org.createGoal": "创建目标",
|
||||
@@ -611,8 +623,8 @@ export const zh: Record<string, string> = {
|
||||
"org.fit": "适配",
|
||||
|
||||
// ── 运行 ──
|
||||
"runs.noRunsYet": "暂无运行",
|
||||
"runs.noRunsDetail": "任务执行后运行记录将显示在这里。",
|
||||
"runs.noRunsYet": "暂无智能体运行记录",
|
||||
"runs.noRunsDetail": "智能体执行任务时将创建运行记录,包含日志和状态信息。",
|
||||
"runs.activeLabel": "活跃",
|
||||
"runs.queuedOrRunning": "正在排队或运行中",
|
||||
"runs.needsAttention": "需要关注",
|
||||
@@ -622,7 +634,7 @@ export const zh: Record<string, string> = {
|
||||
"runs.runQueue": "运行队列",
|
||||
"runs.searchRuns": "按任务、智能体、连接器、工作区或错误搜索",
|
||||
"runs.noRunsMatch": "没有匹配此筛选条件的运行",
|
||||
"runs.noRunsMatchDetail": "尝试调整筛选条件。",
|
||||
"runs.noRunsMatchDetail": "尝试放宽筛选条件或清除筛选以查看所有运行。",
|
||||
"runs.runDetail": "运行详情",
|
||||
"runs.openConsole": "打开控制台",
|
||||
"runs.cancelRun": "取消运行",
|
||||
|
||||
@@ -3,6 +3,14 @@ import type { AppState } from "../store";
|
||||
import type { AppLocale } from "@shared/locale";
|
||||
import { DEFAULT_LOCALE } from "@shared/locale";
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
tone: "success" | "danger" | "warn";
|
||||
}
|
||||
|
||||
function nextToastId() { return `t-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; }
|
||||
|
||||
export interface UiSlice {
|
||||
locale: AppLocale;
|
||||
showOnboarding: boolean;
|
||||
@@ -10,12 +18,15 @@ export interface UiSlice {
|
||||
confirmDeleteCompanyId: string | null;
|
||||
taskViewMode: "list" | "kanban";
|
||||
logRevision: number;
|
||||
toasts: Toast[];
|
||||
setLocale: (locale: AppLocale) => void;
|
||||
setShowOnboarding: (show: boolean) => void;
|
||||
setInitialOnboardingLocked: (locked: boolean) => void;
|
||||
setConfirmDeleteCompanyId: (id: string | null) => void;
|
||||
setTaskViewMode: (mode: "list" | "kanban") => void;
|
||||
incrementLogRevision: () => void;
|
||||
addToast: (message: string, tone?: Toast["tone"]) => void;
|
||||
dismissToast: (id: string) => void;
|
||||
}
|
||||
|
||||
export const createUiSlice: StateCreator<
|
||||
@@ -30,6 +41,7 @@ export const createUiSlice: StateCreator<
|
||||
confirmDeleteCompanyId: null,
|
||||
taskViewMode: "list",
|
||||
logRevision: 0,
|
||||
toasts: [],
|
||||
setLocale: (locale) =>
|
||||
set((state) => {
|
||||
state.locale = locale;
|
||||
@@ -54,4 +66,13 @@ export const createUiSlice: StateCreator<
|
||||
set((state) => {
|
||||
state.logRevision += 1;
|
||||
}),
|
||||
addToast: (message, tone = "success") => {
|
||||
const id = nextToastId();
|
||||
set((state) => { state.toasts.push({ id, message, tone }); });
|
||||
setTimeout(() => {
|
||||
set((state) => { state.toasts = state.toasts.filter((t) => t.id !== id); });
|
||||
}, 4000);
|
||||
},
|
||||
dismissToast: (id) =>
|
||||
set((state) => { state.toasts = state.toasts.filter((t) => t.id !== id); }),
|
||||
});
|
||||
+70
-57
@@ -10,24 +10,27 @@
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #fbfbfb;
|
||||
--bg: #f7f8fa;
|
||||
--bg-subtle: #ffffff;
|
||||
--panel: #ffffff;
|
||||
--panel-soft: #f4f5f5;
|
||||
--panel-strong: #fafafa;
|
||||
--line: rgba(15, 23, 42, 0.08);
|
||||
--line-strong: rgba(15, 23, 42, 0.14);
|
||||
--text: #1a202c;
|
||||
--muted: #718096;
|
||||
--muted-strong: #4a5568;
|
||||
--accent: #c06a3a;
|
||||
--accent-soft: #f7ece6;
|
||||
--danger: #e5484d;
|
||||
--danger-soft: #fdebec;
|
||||
--warn: #b7791f;
|
||||
--warn-soft: #fdf6e3;
|
||||
--success: #2f855a;
|
||||
--success-soft: #edf7f0;
|
||||
--panel-soft: #f0f2f5;
|
||||
--panel-strong: #f7f8fa;
|
||||
--line: rgba(50, 65, 85, 0.08);
|
||||
--line-strong: rgba(50, 65, 85, 0.15);
|
||||
--text: #1a1d24;
|
||||
--muted: #6b7280;
|
||||
--muted-strong: #4b5563;
|
||||
--accent: #4a5d7a;
|
||||
--accent-text: #3d5070;
|
||||
--accent-soft: #edf1f7;
|
||||
--accent-hover: #3d4f6d;
|
||||
--text-on-accent: #ffffff;
|
||||
--danger: #c53030;
|
||||
--danger-soft: #fef2f2;
|
||||
--warn: #b45309;
|
||||
--warn-soft: #fffbeb;
|
||||
--success: #0f766e;
|
||||
--success-soft: #ecfdf5;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
@@ -36,47 +39,53 @@
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #111315;
|
||||
--bg-subtle: #171a1d;
|
||||
--panel: #1b1f23;
|
||||
--panel-soft: #232931;
|
||||
--panel-strong: #20252c;
|
||||
--line: rgba(226, 232, 240, 0.1);
|
||||
--line-strong: rgba(226, 232, 240, 0.18);
|
||||
--text: #f4f7fb;
|
||||
--muted: #9aa7b5;
|
||||
--muted-strong: #c2ccd8;
|
||||
--accent: #ee8a58;
|
||||
--accent-soft: rgba(238, 138, 88, 0.14);
|
||||
--danger: #ff7b86;
|
||||
--danger-soft: rgba(255, 123, 134, 0.14);
|
||||
--warn: #f4bc5b;
|
||||
--warn-soft: rgba(244, 188, 91, 0.14);
|
||||
--success: #61d69d;
|
||||
--success-soft: rgba(97, 214, 157, 0.14);
|
||||
--bg: #0f1117;
|
||||
--bg-subtle: #161820;
|
||||
--panel: #1b1e26;
|
||||
--panel-soft: #252830;
|
||||
--panel-strong: #1e2129;
|
||||
--line: rgba(148, 163, 190, 0.1);
|
||||
--line-strong: rgba(148, 163, 190, 0.18);
|
||||
--text: #edf0f7;
|
||||
--muted: #8b95a5;
|
||||
--muted-strong: #b0b8c8;
|
||||
--accent: #7b9cc4;
|
||||
--accent-text: #7b9cc4;
|
||||
--accent-soft: rgba(123, 156, 196, 0.14);
|
||||
--accent-hover: #93b0d4;
|
||||
--text-on-accent: #ffffff;
|
||||
--danger: #f87171;
|
||||
--danger-soft: rgba(248, 113, 113, 0.12);
|
||||
--warn: #f59e0b;
|
||||
--warn-soft: rgba(245, 158, 11, 0.12);
|
||||
--success: #34d399;
|
||||
--success-soft: rgba(52, 211, 153, 0.12);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="system"] {
|
||||
color-scheme: dark;
|
||||
--bg: #111315;
|
||||
--bg-subtle: #171a1d;
|
||||
--panel: #1b1f23;
|
||||
--panel-soft: #232931;
|
||||
--panel-strong: #20252c;
|
||||
--line: rgba(226, 232, 240, 0.1);
|
||||
--line-strong: rgba(226, 232, 240, 0.18);
|
||||
--text: #f4f7fb;
|
||||
--muted: #9aa7b5;
|
||||
--muted-strong: #c2ccd8;
|
||||
--accent: #ee8a58;
|
||||
--accent-soft: rgba(238, 138, 88, 0.14);
|
||||
--danger: #ff7b86;
|
||||
--danger-soft: rgba(255, 123, 134, 0.14);
|
||||
--warn: #f4bc5b;
|
||||
--warn-soft: rgba(244, 188, 91, 0.14);
|
||||
--success: #61d69d;
|
||||
--success-soft: rgba(97, 214, 157, 0.14);
|
||||
--bg: #0f1117;
|
||||
--bg-subtle: #161820;
|
||||
--panel: #1b1e26;
|
||||
--panel-soft: #252830;
|
||||
--panel-strong: #1e2129;
|
||||
--line: rgba(148, 163, 190, 0.1);
|
||||
--line-strong: rgba(148, 163, 190, 0.18);
|
||||
--text: #edf0f7;
|
||||
--muted: #8b95a5;
|
||||
--muted-strong: #b0b8c8;
|
||||
--accent: #7b9cc4;
|
||||
--accent-text: #7b9cc4;
|
||||
--accent-soft: rgba(123, 156, 196, 0.14);
|
||||
--accent-hover: #93b0d4;
|
||||
--text-on-accent: #ffffff;
|
||||
--danger: #f87171;
|
||||
--danger-soft: rgba(248, 113, 113, 0.12);
|
||||
--warn: #f59e0b;
|
||||
--warn-soft: rgba(245, 158, 11, 0.12);
|
||||
--success: #34d399;
|
||||
--success-soft: rgba(52, 211, 153, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +133,11 @@ select {
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(113, 128, 150, 0.3);
|
||||
background: rgba(60, 75, 100, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(60, 75, 100, 0.32);
|
||||
}
|
||||
|
||||
.mono {
|
||||
@@ -134,7 +147,7 @@ select {
|
||||
.surface {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
box-shadow: 0 1px 3px rgba(26, 29, 36, 0.06), 0 0 0 0.5px rgba(26, 29, 36, 0.03);
|
||||
}
|
||||
|
||||
.focus-ring {
|
||||
@@ -147,9 +160,9 @@ select {
|
||||
|
||||
.focus-ring:focus-visible {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(192, 106, 58, 0.5),
|
||||
0 0 0 4px rgba(192, 106, 58, 0.12);
|
||||
border-color: rgba(192, 106, 58, 0.35);
|
||||
0 0 0 1px rgba(74, 93, 122, 0.5),
|
||||
0 0 0 4px rgba(74, 93, 122, 0.12);
|
||||
border-color: rgba(74, 93, 122, 0.35);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
Reference in new issue
Block a user