update
This commit is contained in:
1 parent
806200b3d1
commit
5547c29491
216 files changed
+5043
-3352
No files matched your search
@@ -1,12 +1,13 @@
|
||||
import { CaretDown, CaretRight } from "../ui/icons";
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
import { Input } from "../ui";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Shared */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const INPUT_CLS =
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring w-full rounded-lg border border-border bg-card px-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground";
|
||||
export { Input };
|
||||
|
||||
export interface ModelEntry {
|
||||
id: string;
|
||||
@@ -25,12 +26,13 @@ export function parseEnvValue(text: string, key: string): string {
|
||||
/* StatusBadge */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boolean }) {
|
||||
export function StatusBadge({ status }: { status: ConnectorStatus }) {
|
||||
const t = useT();
|
||||
if (status === "ready") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-success-muted px-2 py-0.5 text-[11px] font-medium text-success">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||
{zh ? "就绪" : "Ready"}
|
||||
{t("models.ready")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +40,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground" />
|
||||
{zh ? "未安装" : "Not installed"}
|
||||
{t("models.notInstalled")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +48,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-warning-muted px-2 py-0.5 text-[11px] font-medium text-warning">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-warning" />
|
||||
{zh ? "需登录" : "Auth required"}
|
||||
{t("models.authRequired")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -54,7 +56,7 @@ export function StatusBadge({ status, zh }: { status: ConnectorStatus; zh: boole
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
||||
<span className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-muted-foreground border-t-primary" />
|
||||
{zh ? "检测中" : "Checking"}
|
||||
{t("models.checking")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -137,18 +139,17 @@ export function ModelToggleList({
|
||||
onToggle,
|
||||
onRemove,
|
||||
canRemove,
|
||||
zh,
|
||||
}: {
|
||||
models: ModelEntry[];
|
||||
onToggle: (id: string) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
canRemove: boolean;
|
||||
zh: boolean;
|
||||
}) {
|
||||
const t = useT();
|
||||
if (models.length === 0) {
|
||||
return (
|
||||
<div className="text-[12px] text-muted-foreground">
|
||||
{zh ? "暂无模型" : "No models available"}
|
||||
{t("models.noModels")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -156,7 +157,7 @@ export function ModelToggleList({
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">
|
||||
{zh ? "模型" : "Models"} ({enabledCount}/{models.length} {zh ? "已启用" : "enabled"})
|
||||
{t("models.label")} ({t("models.enabledCount").replace("{enabled}", String(enabledCount)).replace("{total}", String(models.length))})
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{models.map((m) => (
|
||||
@@ -168,7 +169,7 @@ export function ModelToggleList({
|
||||
<ToggleSwitch
|
||||
checked={m.enabled}
|
||||
onChange={() => onToggle(m.id)}
|
||||
label={`${m.enabled ? "Disable" : "Enable"} ${m.label}`}
|
||||
label={(m.enabled ? t("models.disableToggle") : t("models.enableToggle")).replace("{name}", m.label)}
|
||||
/>
|
||||
<span
|
||||
className={`text-[12px] ${m.enabled ? "text-foreground" : "text-muted-foreground line-through"}`}
|
||||
@@ -181,7 +182,7 @@ export function ModelToggleList({
|
||||
type="button"
|
||||
onClick={() => onRemove(m.id)}
|
||||
className="text-muted-foreground transition hover:text-destructive"
|
||||
aria-label={`Remove ${m.label}`}
|
||||
aria-label={t("models.removeModel").replace("{name}", m.label)}
|
||||
>
|
||||
<XIcon size={12} />
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import type { BudgetPolicyRecord } from "@shared/types";
|
||||
import { ActionButton, Input, Modal, Select } from "../ui";
|
||||
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
interface PolicyFormModalProps {
|
||||
open: boolean;
|
||||
@@ -14,6 +15,7 @@ interface PolicyFormModalProps {
|
||||
}
|
||||
|
||||
export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPolicy, agents, projects }: PolicyFormModalProps) {
|
||||
const t = useT();
|
||||
const [scopeType, setScopeType] = useState<"company" | "agent" | "project">("company");
|
||||
const [scopeId, setScopeId] = useState(companyId);
|
||||
const [amountDollars, setAmountDollars] = useState("100");
|
||||
@@ -79,23 +81,23 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={editingPolicy ? "Edit Budget Policy" : "Add Budget Policy"}
|
||||
title={editingPolicy ? t("policy.editTitle") : t("policy.addTitle")}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<Select
|
||||
label="Scope"
|
||||
label={t("policy.scope")}
|
||||
value={scopeType}
|
||||
onChange={(v) => setScopeType(v as "company" | "agent" | "project")}
|
||||
options={[
|
||||
{ value: "company", label: "Company" },
|
||||
{ value: "agent", label: "Agent" },
|
||||
{ value: "project", label: "Project" },
|
||||
{ value: "company", label: t("policy.scopeCompany") },
|
||||
{ value: "agent", label: t("policy.scopeAgent") },
|
||||
{ value: "project", label: t("policy.scopeProject") },
|
||||
]}
|
||||
/>
|
||||
|
||||
{scopeType === "agent" ? (
|
||||
<Select
|
||||
label="Agent"
|
||||
label={t("policy.scopeAgent")}
|
||||
value={scopeId}
|
||||
onChange={setScopeId}
|
||||
options={agents.map((a) => ({ value: a.id, label: a.name }))}
|
||||
@@ -104,7 +106,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
|
||||
{scopeType === "project" ? (
|
||||
<Select
|
||||
label="Project"
|
||||
label={t("policy.scopeProject")}
|
||||
value={scopeId}
|
||||
onChange={setScopeId}
|
||||
options={projects.map((p) => ({ value: p.id, label: p.name }))}
|
||||
@@ -112,7 +114,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
label="Monthly Budget (USD)"
|
||||
label={t("policy.monthlyBudget")}
|
||||
value={amountDollars}
|
||||
onChange={setAmountDollars}
|
||||
type="number"
|
||||
@@ -120,7 +122,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Warning Threshold (%)"
|
||||
label={t("policy.warningThreshold")}
|
||||
value={warnPercent}
|
||||
onChange={setWarnPercent}
|
||||
type="number"
|
||||
@@ -134,7 +136,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
onChange={(e) => setHardStopEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
|
||||
/>
|
||||
<span className="text-[13px] text-foreground">Enable hard stop (block new runs at 100%)</span>
|
||||
<span className="text-[13px] text-foreground">{t("policy.hardStop")}</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2.5">
|
||||
@@ -144,19 +146,13 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
|
||||
onChange={(e) => setNotifyEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
|
||||
/>
|
||||
<span className="text-[13px] text-foreground">Send notifications</span>
|
||||
<span className="text-[13px] text-foreground">{t("policy.sendNotifications")}</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition hover:text-foreground"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<ActionButton label={t("policy.cancel")} onClick={onClose} />
|
||||
<ActionButton
|
||||
label={saving ? "Saving..." : "Save Policy"}
|
||||
label={saving ? t("policy.saving") : t("policy.savePolicy")}
|
||||
tone="accent"
|
||||
disabled={saving}
|
||||
onClick={() => void handleSave()}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useLocale, useT } from "../../i18n"
|
||||
import { formatTime } from "../../lib/formatters"
|
||||
import { unwrap } from "../../lib/desktop"
|
||||
import { isMac } from "../../lib/platform"
|
||||
import { ActionButton, ConfirmDialog, EmptyState, InlineNotice, Select } from "../ui"
|
||||
import { ActionButton, ConfirmDialog, EmptyState, Eyebrow, InlineNotice, Select } from "../ui"
|
||||
import { describeActionError } from "./execution-utils"
|
||||
import { useAppStore } from "../../state/store"
|
||||
|
||||
@@ -19,25 +19,25 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
|
||||
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."); 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}.`); 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) } }
|
||||
async function handleDeleteSecret(id: string) { setDeleteSecretError(null); setActionError(null); setActionNotice(null); try { unwrap(await window.agentCompany.deleteSecret({ id, companyId })); setConfirmDeleteSecretId(null); setActionNotice(t("settings.secretDeleted")); addToast(t("settings.secretDeleted"), "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(t("settings.settingsSaved")); addToast(t("settings.settingsSaved"), "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 ? t("settings.updateCheckStarted") : t("settings.updateCheckSkipped")) } 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(t("settings.profileBackupAt").replace("{path}", backupPath)); addToast(t("settings.profileBackupCreated"), "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(t("settings.profileRestored")); addToast(t("settings.profileRestored"), "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) }} />
|
||||
<ConfirmDialog open={confirmDeleteSecretId !== null} title={t("settings.deleteSecretTitle")} message={t("settings.deleteSecretConfirm").replace("{name}", secretToDelete?.name ?? "")} 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-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.profile")}</div><div className="border-y border-border py-4"><div className="mb-2 text-[11px] text-muted-foreground">{t("exec.profilePath")}</div><div className="font-mono truncate text-[11px] text-muted-foreground">{snapshot.profilePath}</div></div></div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.preferences")}</div>
|
||||
<div><Eyebrow className="mb-3">{t("exec.profile")}</Eyebrow><div className="border-y border-border py-4"><div className="mb-2 text-[11px] text-muted-foreground">{t("exec.profilePath")}</div><div className="font-mono truncate text-[11px] text-muted-foreground">{snapshot.profilePath}</div></div></div>
|
||||
<div><Eyebrow className="mb-3">{t("exec.preferences")}</Eyebrow>
|
||||
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.className = 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-border py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-muted-foreground"><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; const isCeo = company.executionMode !== "ai_cofounder"; return (
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.executionMode")}</div>
|
||||
<div><Eyebrow className="mb-3">{t("exec.executionMode")}</Eyebrow>
|
||||
<div className="space-y-3">
|
||||
<button type="button" onClick={async () => { if (isCeo) return; setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: true, reviewDeliverables: false, executionMode: "ai_ceo" })); await onSaved(); addToast(t("exec.modeSwitched"), "success") } catch (err) { setActionError(describeActionError(err)) } }} className={`w-full rounded-lg border-2 px-4 py-3 text-left transition ${isCeo ? "border-[color:var(--primary)] bg-accent" : "border-border bg-muted hover:border-[color:var(--muted-foreground)] cursor-pointer"}`}>
|
||||
<div className="text-[13px] font-semibold text-foreground">AI CEO <span className="ml-1 text-[10px] font-normal text-muted-foreground">{t("exec.modeCeoTag")}</span></div>
|
||||
@@ -48,15 +48,15 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
|
||||
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("exec.modeCofounderDesc")}</div>
|
||||
</button>
|
||||
</div></div>) })()}
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Keyboard Shortcuts</div>
|
||||
<div><Eyebrow className="mb-3">{t("settings.keyboardShortcuts")}</Eyebrow>
|
||||
<div className="space-y-1 border-y border-border py-3">
|
||||
{([
|
||||
[isMac() ? "⌘K" : "Ctrl+K", "Open command palette"],
|
||||
[isMac() ? "⌘Enter" : "Ctrl+Enter", "Send message"],
|
||||
["Escape", "Close modal / palette"],
|
||||
["↑ / ↓", "Navigate palette results"],
|
||||
["Enter", "Select palette result"],
|
||||
["Tab", "Move focus between elements"],
|
||||
[isMac() ? "⌘K" : "Ctrl+K", t("settings.shortcut.commandPalette")],
|
||||
[isMac() ? "⌘Enter" : "Ctrl+Enter", t("settings.shortcut.sendMessage")],
|
||||
["Escape", t("settings.shortcut.closeModal")],
|
||||
["↑ / ↓", t("settings.shortcut.navigatePalette")],
|
||||
["Enter", t("settings.shortcut.selectPaletteResult")],
|
||||
["Tab", t("settings.shortcut.moveFocus")],
|
||||
] as const).map(([key, desc]) => (
|
||||
<div key={key} className="flex items-center justify-between px-3 py-1.5 text-[12px]">
|
||||
<span className="text-muted-foreground">{desc}</span>
|
||||
@@ -65,9 +65,9 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{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-muted-foreground">{t("exec.secretBackend")}</div><div className="border-y border-border py-3 text-[13px] text-muted-foreground">{snapshot.backend.secretBackend}</div></div>
|
||||
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-border overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-border"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-border last:border-b-0"><td className="px-4 py-3 font-medium text-foreground">{s.name}</td><td className="px-4 py-3 text-muted-foreground">{s.description || "\u2014"}</td><td className="px-4 py-3 font-mono text-muted-foreground">{s.backend}</td><td className="px-4 py-3 font-mono text-muted-foreground">{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><Eyebrow className="mb-3">{t("exec.actions")}</Eyebrow><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><Eyebrow className="mb-3">{t("exec.secretBackend")}</Eyebrow><div className="border-y border-border py-3 text-[13px] text-muted-foreground">{snapshot.backend.secretBackend}</div></div>
|
||||
<div><Eyebrow className="mb-3">{t("exec.secrets")} ({secrets.length})</Eyebrow>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-border overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-border"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-border last:border-b-0"><td className="px-4 py-3 font-medium text-foreground">{s.name}</td><td className="px-4 py-3 text-muted-foreground">{s.description || "\u2014"}</td><td className="px-4 py-3 font-mono text-muted-foreground">{s.backend}</td><td className="px-4 py-3 font-mono text-muted-foreground">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label={t("settings.delete")} onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { CommentRecord, GoalRecord, ProfileSnapshot, ProjectRecord, RunReco
|
||||
import { useT } from "../../i18n"
|
||||
import { formatMoney, formatTime } from "../../lib/formatters"
|
||||
import { unwrap } from "../../lib/desktop"
|
||||
import { ActionButton, ConfirmDialog, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
|
||||
import { ActionButton, ConfirmDialog, Eyebrow, InlineNotice, Input, Select, StatusPill, TextArea } from "../ui"
|
||||
import { describeActionError } from "./execution-utils"
|
||||
import { TaskComments } from "./TaskComments"
|
||||
import { useAppStore } from "../../state/store"
|
||||
@@ -80,7 +80,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
)}
|
||||
{(() => { const subtasks = tasks.filter((t) => t.parentId === task.id); if (subtasks.length === 0) return null; return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-success">{t("exec.doneCount").replace("{0}", String(subtasks.filter((st) => st.status === "done").length)).replace("{1}", String(subtasks.length))}</span></div>
|
||||
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><Eyebrow>{t("exec.subtasks")} ({subtasks.length})</Eyebrow><span className="text-[10px] text-success">{t("exec.doneCount").replace("{0}", String(subtasks.filter((st) => st.status === "done").length)).replace("{1}", String(subtasks.length))}</span></div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-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 = agentMap.get(sub.assigneeAgentId ?? ""); return (
|
||||
<div key={sub.id} className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12px] bg-muted">
|
||||
@@ -89,7 +89,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
|
||||
{subAssignee ? <span className="shrink-0 text-[10px] text-muted-foreground">{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-semibold uppercase tracking-[0.08em] text-muted-foreground">{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 (
|
||||
{runs.length > 0 && <div className="space-y-2"><Eyebrow>{t("exec.runHistory")}</Eyebrow><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-lg border px-3 py-2.5 ${isActive ? "border-[color:var(--primary)] bg-success-muted" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--destructive)] border-opacity-30 bg-destructive-muted" : "border-border bg-muted"}`}>
|
||||
<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-success" /> : null}<span className="truncate text-[13px] font-medium text-foreground">{run.summary || (isActive ? t("exec.runningEllipsis") : t("exec.completedRun"))}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="font-mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="font-mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
|
||||
{run.errorMessage ? <div className="mt-1.5 rounded bg-destructive-muted px-2 py-1 text-[11px] text-destructive break-words">{run.errorMessage}</div> : null}
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { ApprovalRecord, DocumentRecord } from "@shared/types"
|
||||
import { extractApprovalDocumentReferences, type ApprovalDocumentReference } from "@shared/approval-documents"
|
||||
import { unwrap } from "../../../lib/desktop"
|
||||
import { useAppStore } from "../../../state/store"
|
||||
import { ActionButton, InlineNotice, Input, TextArea } from "../../ui"
|
||||
import { ActionButton, Card, Eyebrow, InlineNotice, Input, TextArea } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
import { describeActionError } from "../execution-utils"
|
||||
|
||||
function ApprovalDocumentEditor({
|
||||
@@ -16,6 +17,7 @@ function ApprovalDocumentEditor({
|
||||
document: DocumentRecord
|
||||
fallback?: ApprovalDocumentReference
|
||||
}) {
|
||||
const t = useT()
|
||||
const addToast = useAppStore((s) => s.addToast)
|
||||
const [title, setTitle] = useState(document.title)
|
||||
const [content, setContent] = useState(document.content ?? "")
|
||||
@@ -24,7 +26,7 @@ function ApprovalDocumentEditor({
|
||||
|
||||
async function handleSave() {
|
||||
if (!title.trim()) {
|
||||
setSaveError("Document title is required.")
|
||||
setSaveError(t("approval.documentTitleRequired"))
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
@@ -47,7 +49,7 @@ function ApprovalDocumentEditor({
|
||||
tagsJson: document.tagsJson,
|
||||
}),
|
||||
)
|
||||
addToast(`Saved document: ${title.trim()}`, "success")
|
||||
addToast(t("approval.documentSavedToast").replace("{title}", title.trim()), "success")
|
||||
} catch (error) {
|
||||
setSaveError(describeActionError(error))
|
||||
} finally {
|
||||
@@ -56,7 +58,7 @@ function ApprovalDocumentEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-border px-4 py-3">
|
||||
<Card padding="sm" className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{fallback?.title ?? document.title}</div>
|
||||
@@ -65,7 +67,7 @@ function ApprovalDocumentEditor({
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
label={saving ? "Saving..." : "Save changes"}
|
||||
label={saving ? t("approval.saving") : t("approval.saveChanges")}
|
||||
icon={CheckCircle}
|
||||
tone="accent"
|
||||
onClick={() => void handleSave()}
|
||||
@@ -73,9 +75,9 @@ function ApprovalDocumentEditor({
|
||||
/>
|
||||
</div>
|
||||
{saveError ? <InlineNotice message={saveError} /> : null}
|
||||
<Input label="Title" value={title} onChange={setTitle} placeholder="Document title" />
|
||||
<TextArea label="Content" value={content} onChange={setContent} rows={12} placeholder="Document content" />
|
||||
</div>
|
||||
<Input label={t("approval.docTitle")} value={title} onChange={setTitle} placeholder={t("approval.docTitlePlaceholder")} />
|
||||
<TextArea label={t("approval.docContent")} value={content} onChange={setContent} rows={12} placeholder={t("approval.docContentPlaceholder")} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,6 +90,7 @@ export function ApprovalDocumentsPanel({
|
||||
documents?: DocumentRecord[]
|
||||
companyId: string
|
||||
}) {
|
||||
const t = useT()
|
||||
const references = useMemo(
|
||||
() => extractApprovalDocumentReferences(approval.payloadJson),
|
||||
[approval.payloadJson],
|
||||
@@ -109,7 +112,7 @@ export function ApprovalDocumentsPanel({
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Linked Documents</div>
|
||||
<Eyebrow tone="primary">{t("approval.linkedDocuments")}</Eyebrow>
|
||||
{fullDocs.length > 0
|
||||
? fullDocs.map(({ reference, document }) => (
|
||||
<ApprovalDocumentEditor
|
||||
@@ -120,15 +123,15 @@ export function ApprovalDocumentsPanel({
|
||||
/>
|
||||
))
|
||||
: references.map((reference) => (
|
||||
<div key={reference.id} className="rounded-lg border border-border bg-muted px-4 py-3">
|
||||
<Card key={reference.id} padding="sm" className="bg-muted">
|
||||
<div className="text-[13px] font-medium text-foreground">{reference.title ?? reference.id}</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{reference.type?.replaceAll("_", " ") ?? "document"} · {reference.id}
|
||||
</div>
|
||||
<div className="mt-2 text-[12px] text-muted-foreground">
|
||||
The linked document is not loaded in the current snapshot yet. Reopen the approval after the next refresh.
|
||||
{t("approval.documentNotLoaded")}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{approval.impactSummary ? (
|
||||
<div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AgentRecord, ApprovalRecord, DocumentRecord, TaskRecord } from "@s
|
||||
import { extractApprovalDocumentReferences } from "@shared/approval-documents"
|
||||
import { useT } from "../../../i18n"
|
||||
import { unwrap } from "../../../lib/desktop"
|
||||
import { ConfirmDialog, InlineNotice } from "../../ui"
|
||||
import { Card, ConfirmDialog, Eyebrow, InlineNotice } from "../../ui"
|
||||
import { useAppStore } from "../../../state/store"
|
||||
import { describeActionError } from "../execution-utils"
|
||||
import { SocialDraftPreview } from "../SocialDraftPreview"
|
||||
@@ -69,12 +69,12 @@ export function ApprovalInspector({
|
||||
|
||||
if (isSocialPost && state === "approved" && scheduleMode === "scheduled") {
|
||||
if (!scheduledAt) {
|
||||
setScheduleError("Please select a date and time for scheduling.")
|
||||
setScheduleError(t("approval.scheduleSelectTime"))
|
||||
return
|
||||
}
|
||||
const chosenTime = new Date(scheduledAt).getTime()
|
||||
if (chosenTime <= Date.now()) {
|
||||
setScheduleError("Scheduled time must be in the future.")
|
||||
setScheduleError(t("approval.scheduleFuture"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,9 @@ export function ApprovalInspector({
|
||||
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")
|
||||
if (state === "approved") addToast(t("approval.granted"), "success")
|
||||
else if (state === "rejected") addToast(t("approval.rejectedToast"), "warn")
|
||||
else if (state === "revision_requested") addToast(t("approval.revisionRequested"), "warn")
|
||||
} catch (error) {
|
||||
setActionError(describeActionError(error))
|
||||
} finally {
|
||||
@@ -115,14 +115,17 @@ export function ApprovalInspector({
|
||||
<ApprovalSummary approval={approval} requestingAgent={requestingAgent} relatedTask={relatedTask} />
|
||||
{approval.type === "hire_agent" ? <ProposedHireCard approval={approval} agents={agents} /> : null}
|
||||
{approval.type === "approve_ceo_strategy" ? (
|
||||
<div className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-warning-muted px-4 py-3">
|
||||
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-warning">
|
||||
Organization Strategy
|
||||
</div>
|
||||
<Card
|
||||
padding="sm"
|
||||
className="border-[color:var(--destructive)] border-opacity-30 bg-warning-muted"
|
||||
>
|
||||
<Eyebrow tone="warning" className="mb-2">
|
||||
{t("approval.orgStrategy")}
|
||||
</Eyebrow>
|
||||
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
|
||||
{approval.payloadSummary}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
<ApprovalDocumentsPanel approval={approval} documents={documents} companyId={companyId} />
|
||||
{approval.type === "deliverable_review" && !hasDocumentReferences ? (
|
||||
@@ -160,7 +163,7 @@ export function ApprovalInspector({
|
||||
|
||||
{approval.decisionNote ? (
|
||||
<div className="border-t border-border pt-4 text-[13px] text-muted-foreground">
|
||||
Last note: {approval.decisionNote}
|
||||
{t("approval.lastNote")} {approval.decisionNote}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Briefcase, Robot } from "../../ui/icons"
|
||||
import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types"
|
||||
import { formatTime } from "../../../lib/formatters"
|
||||
import { StatusPill } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
import { TypeBadge } from "./TypeBadge"
|
||||
|
||||
export function ApprovalSummary({
|
||||
@@ -13,6 +14,7 @@ export function ApprovalSummary({
|
||||
requestingAgent: AgentRecord | null
|
||||
relatedTask: TaskRecord | null
|
||||
}) {
|
||||
const t = useT()
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -26,8 +28,8 @@ export function ApprovalSummary({
|
||||
<div className="grid gap-2 text-[13px] md:grid-cols-2">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Robot size={14} />
|
||||
Requested by:{" "}
|
||||
<span className="font-medium text-foreground">{requestingAgent?.name ?? "Unknown"}</span>
|
||||
{t("approval.requestedBy")}{" "}
|
||||
<span className="font-medium text-foreground">{requestingAgent?.name ?? t("approval.unknownRequester")}</span>
|
||||
{requestingAgent?.role ? (
|
||||
<span className="text-[10px] text-muted-foreground">({requestingAgent.role})</span>
|
||||
) : null}
|
||||
@@ -35,15 +37,15 @@ export function ApprovalSummary({
|
||||
{relatedTask ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Briefcase size={14} />
|
||||
Task: <span className="font-medium text-foreground">{relatedTask.title}</span>
|
||||
{t("approval.task")} <span className="font-medium text-foreground">{relatedTask.title}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-muted-foreground">
|
||||
Created: <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span>
|
||||
{t("approval.created")} <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span>
|
||||
</div>
|
||||
{approval.updatedAt !== approval.createdAt ? (
|
||||
<div className="text-muted-foreground">
|
||||
Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span>
|
||||
{t("approval.updated")} <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CheckCircle, ClockCounterClockwise, Trash, XCircle } from "../../ui/icons"
|
||||
import { ActionButton, TextArea } from "../../ui"
|
||||
import { ActionButton, Eyebrow, TextArea } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
|
||||
export type DecisionState = "approved" | "rejected" | "revision_requested"
|
||||
@@ -28,13 +28,13 @@ export function DecisionForm({
|
||||
<>
|
||||
{!isDecided ? (
|
||||
<div>
|
||||
<label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Decision note
|
||||
</label>
|
||||
<Eyebrow className="mb-1">
|
||||
{t("approval.decisionNote")}
|
||||
</Eyebrow>
|
||||
<TextArea
|
||||
value={decisionNote}
|
||||
onChange={onDecisionNoteChange}
|
||||
placeholder="Add a note (optional)"
|
||||
placeholder={t("approval.decisionNotePlaceholder")}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ export function DecisionForm({
|
||||
{!isDecided ? (
|
||||
<>
|
||||
<ActionButton
|
||||
label={isSocialPost ? "Approve & Post" : t("exec.approve")}
|
||||
label={isSocialPost ? t("approval.approvePost") : t("exec.approve")}
|
||||
tone="accent"
|
||||
onClick={() => onDecide("approved")}
|
||||
icon={CheckCircle}
|
||||
@@ -66,7 +66,7 @@ export function DecisionForm({
|
||||
) : null}
|
||||
{isDecided ? (
|
||||
<ActionButton
|
||||
label="Delete"
|
||||
label={t("approval.delete")}
|
||||
onClick={onRequestDelete}
|
||||
icon={Trash}
|
||||
tone="danger"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { CaretDown, CaretUp } from "../../ui/icons"
|
||||
import type { AgentRecord, ApprovalRecord, DocumentRecord } from "@shared/types"
|
||||
import { Card, Eyebrow } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
import { useApprovalPayload } from "./useApprovalPayload"
|
||||
|
||||
interface DocRef {
|
||||
@@ -36,6 +38,7 @@ export function DeliverableReviewPanel({
|
||||
documents?: DocumentRecord[]
|
||||
agents: AgentRecord[]
|
||||
}) {
|
||||
const t = useT()
|
||||
const payload = useApprovalPayload(approval)
|
||||
const docRefs = useMemo(() => toDocRefs(payload?.documents), [payload])
|
||||
const docIds = useMemo(() => toDocIds(payload?.documentIds, docRefs), [payload, docRefs])
|
||||
@@ -52,13 +55,13 @@ export function DeliverableReviewPanel({
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Deliverable Review</div>
|
||||
<Eyebrow tone="primary">{t("approval.deliverableReview")}</Eyebrow>
|
||||
{fullDocs.length > 0 ? (
|
||||
fullDocs.map((doc) => {
|
||||
const isExpanded = expandedDocId === doc.id
|
||||
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
|
||||
return (
|
||||
<div key={doc.id} className="rounded-lg border border-border overflow-hidden">
|
||||
<Card key={doc.id} padding="none" className="overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
|
||||
@@ -78,28 +81,29 @@ export function DeliverableReviewPanel({
|
||||
<div className="border-t border-border px-4 py-3">
|
||||
{author ? (
|
||||
<div className="mb-2 text-[11px] text-muted-foreground">
|
||||
Author: <span className="font-medium text-foreground">{author.name}</span> ({author.role})
|
||||
{t("approval.hireAuthor")} <span className="font-medium text-foreground">{author.name}</span> ({author.role})
|
||||
</div>
|
||||
) : null}
|
||||
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">
|
||||
{doc.content || "No content."}
|
||||
{doc.content || t("approval.docEmpty")}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
) : docRefs.length > 0 ? (
|
||||
docRefs.map((doc) => (
|
||||
<div
|
||||
<Card
|
||||
key={doc.id}
|
||||
className="flex items-center gap-2 rounded-lg border border-border bg-muted px-4 py-3 text-[12px]"
|
||||
padding="sm"
|
||||
className="flex items-center gap-2 bg-muted text-[12px]"
|
||||
>
|
||||
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">
|
||||
{doc.type.replaceAll("_", " ")}
|
||||
</span>
|
||||
<span className="font-medium text-foreground">{doc.title}</span>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMemo } from "react"
|
||||
import type { AgentRecord, ApprovalRecord } from "@shared/types"
|
||||
import { formatMoney } from "../../../lib/formatters"
|
||||
import { Card, Eyebrow } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
import { useApprovalPayload } from "./useApprovalPayload"
|
||||
|
||||
interface HireSnapshot {
|
||||
@@ -25,6 +27,7 @@ export function ProposedHireCard({
|
||||
approval: ApprovalRecord
|
||||
agents: AgentRecord[]
|
||||
}) {
|
||||
const t = useT()
|
||||
const payload = useApprovalPayload(approval)
|
||||
|
||||
const { proposedAgent, reqSnap, mgr } = useMemo(() => {
|
||||
@@ -42,52 +45,52 @@ export function ProposedHireCard({
|
||||
|
||||
if (!proposedAgent && !reqSnap) return null
|
||||
|
||||
const name = proposedAgent?.name ?? reqSnap?.name ?? "Unspecified"
|
||||
const role = proposedAgent?.role ?? reqSnap?.role ?? "Unspecified"
|
||||
const name = proposedAgent?.name ?? reqSnap?.name ?? t("approval.proposedUnspecified")
|
||||
const role = proposedAgent?.role ?? reqSnap?.role ?? t("approval.proposedUnspecified")
|
||||
const title = proposedAgent?.title || reqSnap?.title
|
||||
const capabilities = proposedAgent?.capabilities || reqSnap?.capabilities
|
||||
const connectorId = proposedAgent?.connectorId ?? reqSnap?.connectorId ?? "Unknown"
|
||||
const connectorId = proposedAgent?.connectorId ?? reqSnap?.connectorId ?? t("approval.proposedUnknown")
|
||||
const budget = proposedAgent?.budgetMonthlyUsd ?? reqSnap?.budgetMonthlyUsd ?? 0
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-30 bg-accent px-4 py-3">
|
||||
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Proposed Hire</div>
|
||||
<Card padding="sm" className="border-[color:var(--primary)] border-opacity-30 bg-accent">
|
||||
<Eyebrow tone="primary" className="mb-2">{t("approval.proposedHire")}</Eyebrow>
|
||||
<div className="grid gap-1.5 text-[12px]">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Name:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedName")}</span>{" "}
|
||||
<span className="font-medium text-foreground">{name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Role:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedRole")}</span>{" "}
|
||||
<span className="text-foreground">{role}</span>
|
||||
</div>
|
||||
{title ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Title:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedTitle")}</span>{" "}
|
||||
<span className="text-foreground">{title}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{capabilities ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Capabilities:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedCapabilities")}</span>{" "}
|
||||
<span className="text-foreground">{capabilities}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<span className="text-muted-foreground">Connector:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedConnector")}</span>{" "}
|
||||
<span className="font-mono text-foreground">{connectorId}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Budget:</span>{" "}
|
||||
<span className="font-mono text-foreground">{formatMoney(budget)}/mo</span>
|
||||
<span className="text-muted-foreground">{t("approval.proposedBudget")}</span>{" "}
|
||||
<span className="font-mono text-foreground">{formatMoney(budget)}{t("approval.budgetSuffix")}</span>
|
||||
</div>
|
||||
{mgr ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Reports to:</span>{" "}
|
||||
<span className="text-muted-foreground">{t("approval.proposedReportsTo")}</span>{" "}
|
||||
<span className="text-foreground">{mgr.name}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react"
|
||||
import { Clock } from "../../ui/icons"
|
||||
import { InlineNotice } from "../../ui"
|
||||
import { useT } from "../../../i18n"
|
||||
|
||||
export type ScheduleMode = "immediate" | "scheduled"
|
||||
|
||||
@@ -25,11 +26,12 @@ export function SchedulePicker({
|
||||
onScheduledAtChange: (value: string) => void
|
||||
onClearError: () => void
|
||||
}) {
|
||||
const t = useT()
|
||||
const minDatetime = useMemo(() => computeMinDatetime(), [])
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div role="radiogroup" aria-label="Post timing" className="space-y-2">
|
||||
<div role="radiogroup" aria-label={t("approval.scheduleTimingAria")} className="space-y-2">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -42,7 +44,7 @@ export function SchedulePicker({
|
||||
}}
|
||||
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
|
||||
/>
|
||||
<span className="text-[13px] font-medium text-foreground">Post immediately</span>
|
||||
<span className="text-[13px] font-medium text-foreground">{t("approval.schedulePostNow")}</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
@@ -53,7 +55,7 @@ export function SchedulePicker({
|
||||
onChange={() => onModeChange("scheduled")}
|
||||
className="h-4 w-4 shrink-0 cursor-pointer accent-[color:var(--primary)]"
|
||||
/>
|
||||
<span className="text-[13px] font-medium text-foreground">Schedule for later</span>
|
||||
<span className="text-[13px] font-medium text-foreground">{t("approval.scheduleLater")}</span>
|
||||
</label>
|
||||
</div>
|
||||
{mode === "scheduled" ? (
|
||||
|
||||
Reference in new issue
Block a user