This commit is contained in:
ZacharyZhang-NY committed 2026-04-04 17:01:26 -04:00
1 parent 67c757f481
commit b8dda3c8d0
77 files changed
+1577 -2808

No files matched your search

@@ -1,92 +0,0 @@
import { useEffect } from "react";
import { X } from "@phosphor-icons/react";
import type { AppLocale } from "@shared/locale";
import { useFocusTrap } from "../ui";
interface ClaudeSettingsDialogProps {
claudeApiKey: string;
setClaudeApiKey: (v: string) => void;
claudeBaseUrl: string;
setClaudeBaseUrl: (v: string) => void;
claudeModel: string;
setClaudeModel: (v: string) => void;
onSave: () => void;
onClose: () => void;
saving: boolean;
currentLocale: AppLocale;
}
export function ClaudeSettingsDialog({
claudeApiKey,
setClaudeApiKey,
claudeBaseUrl,
setClaudeBaseUrl,
claudeModel,
setClaudeModel,
onSave,
onClose,
saving,
currentLocale,
}: ClaudeSettingsDialogProps) {
const trapRef = useFocusTrap(true);
useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey) }, [onClose]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/72 backdrop-blur-sm" onClick={onClose}>
<div ref={trapRef} role="dialog" aria-modal="true" aria-label={currentLocale === "zh" ? "Claude Code 设置" : "Claude Code Settings"} className="w-full max-w-[min(420px,90vw)] rounded-lg border border-border bg-card p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[16px] font-semibold text-foreground">Claude Code {currentLocale === "zh" ? "设置" : "Settings"}</h3>
<button type="button" onClick={onClose} className="rounded-lg p-1 text-muted-foreground transition hover:text-foreground"><X size={16} /></button>
</div>
<div className="space-y-4">
<div>
<label htmlFor="claude-api-key" className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">API Key</label>
<input
id="claude-api-key"
type="password"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card px-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeApiKey}
onChange={(e) => setClaudeApiKey(e.target.value)}
placeholder="sk-ant-..."
/>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}</div>
</div>
<div>
<label htmlFor="claude-base-url" className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Base URL</label>
<input
id="claude-base-url"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card px-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeBaseUrl}
onChange={(e) => setClaudeBaseUrl(e.target.value)}
placeholder="https://api.anthropic.com"
/>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}</div>
</div>
<div>
<label htmlFor="claude-model" className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Model</label>
<input
id="claude-model"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card px-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeModel}
onChange={(e) => setClaudeModel(e.target.value)}
placeholder="claude-sonnet-4-20250514"
/>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用连接器默认模型" : "Leave empty for connector default"}</div>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<button type="button" onClick={onClose} className="rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition hover:text-foreground">
{currentLocale === "zh" ? "取消" : "Cancel"}
</button>
<button
type="button"
onClick={onSave}
disabled={saving}
className="rounded-lg bg-primary px-4 py-2 text-[13px] font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50"
>
{saving ? (currentLocale === "zh" ? "保存中..." : "Saving...") : (currentLocale === "zh" ? "保存" : "Save")}
</button>
</div>
</div>
</div>
);
}
@@ -1,84 +0,0 @@
import {
CheckCircle,
GearSix,
WarningCircle,
XCircle,
} from "@phosphor-icons/react";
import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy";
import type { ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
interface ConnectorReadinessProps {
connectors: ProfileSnapshot["connectors"];
currentLocale: AppLocale;
hasRunnableConnector: boolean;
onOpenClaudeSettings: () => void;
t: (key: string) => string;
}
export function ConnectorReadiness({
connectors,
currentLocale,
hasRunnableConnector,
onOpenClaudeSettings,
t,
}: ConnectorReadinessProps) {
return (
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.connectorReadiness")}
</div>
<div className="space-y-2.5">
{connectors.map((connector) => {
const isChecking = connector.status === "detected";
const isReady = connector.status === "ready";
const isMissing = connector.status === "not_installed";
const issue = !isChecking ? getConnectorExecutionReadinessIssue(connector) : null;
return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isChecking ? (
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted-foreground)] border-t-[color:var(--primary)]" />
) : isReady ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-success" />
) : isMissing ? (
<XCircle size={16} weight="fill" className="shrink-0 text-muted-foreground" />
) : (
<WarningCircle size={16} weight="fill" className="shrink-0 text-warning" />
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={isReady ? "font-medium text-foreground" : isChecking ? "text-foreground" : "text-muted-foreground"}>
{connector.label}
</span>
<span className="text-[11px] text-muted-foreground">
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
<div className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">
{issue}
</div>
) : null}
</div>
{connector.id === "claude_local" ? (
<button
type="button"
onClick={(e) => { e.stopPropagation(); onOpenClaudeSettings(); }}
className="shrink-0 rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
title={currentLocale === "zh" ? "Claude Code 设置" : "Claude Code Settings"}
>
<GearSix size={14} />
</button>
) : null}
</div>
);
})}
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-lg border border-[color:var(--destructive)] bg-warning-muted px-4 py-3 text-[12px] text-warning">
{t("onboarding.connectorWarning")}
</div>
) : null}
</div>
);
}
@@ -1,76 +0,0 @@
import { motion } from "framer-motion";
import { CheckCircle, GlobeSimple } from "@phosphor-icons/react";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { ActionButton } from "../ui";
interface LanguageStepProps {
currentLocale: AppLocale;
onLocaleChange: (locale: AppLocale) => void;
onContinue: () => void;
t: (key: string) => string;
}
export function LanguageStep({
currentLocale,
onLocaleChange,
onContinue,
t,
}: LanguageStepProps) {
return (
<motion.div
key="language"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
<GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.selectLanguage")} / Select Language
</div>
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-2.5">
{APP_LOCALES.map((locale) => (
<button
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-lg border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--primary)] bg-accent"
: "border-border bg-muted hover:border-[color:var(--muted-foreground)]"
}`}
>
<div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale
? "border-[color:var(--primary)] bg-primary"
: "border-border"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-primary-foreground" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-foreground" : "text-muted-foreground"
}`}>
{LOCALE_LABELS[locale]}
</span>
</button>
))}
</div>
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={t("onboarding.continue")}
onClick={onContinue}
icon={GlobeSimple}
tone="accent"
/>
</div>
</motion.div>
);
}
@@ -1,19 +1,17 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { Plus, X } from "@phosphor-icons/react";
import { SignIn, X } from "@phosphor-icons/react";
import type { SocialPlatform } from "@shared/types";
import { ActionButton, Input } from "../ui";
import { ActionButton } from "../ui";
import { platformConfig } from "./social-config";
interface AddAccountDialogProps {
onAdd: (platform: SocialPlatform, accountName: string, displayName: string, requireApproval: boolean) => void;
onAddAndLogin: (platform: SocialPlatform, requireApproval: boolean) => void;
onClose: () => void;
}
export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
export function AddAccountDialog({ onAddAndLogin, onClose }: AddAccountDialogProps) {
const [platform, setPlatform] = useState<SocialPlatform>("twitter");
const [accountName, setAccountName] = useState("");
const [displayName, setDisplayName] = useState("");
const [requireApproval, setRequireApproval] = useState(true);
return (
@@ -41,41 +39,29 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Platform</label>
<div className="grid grid-cols-3 gap-1.5">
{(Object.entries(platformConfig) as [SocialPlatform, typeof platformConfig.twitter][]).map(([key, config]) => (
<button
key={key}
type="button"
onClick={() => setPlatform(key)}
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-[11px] font-medium transition ${
platform === key
? "border-[color:var(--primary)] bg-accent text-primary"
: "border-border text-muted-foreground hover:border-border"
}`}
>
<span style={{ color: config.color }}>
<config.icon size={14} />
</span>
{config.label}
</button>
))}
<div className="max-h-[240px] overflow-y-auto rounded-lg border border-border p-1.5">
<div className="grid grid-cols-3 gap-1.5">
{(Object.entries(platformConfig) as [SocialPlatform, typeof platformConfig.twitter][]).map(([key, config]) => (
<button
key={key}
type="button"
onClick={() => setPlatform(key)}
className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-[11px] font-medium transition ${
platform === key
? "border-[color:var(--primary)] bg-accent text-primary"
: "border-border text-muted-foreground hover:border-border"
}`}
>
<span style={{ color: config.color }}>
<config.icon size={14} />
</span>
<span className="truncate">{config.label}</span>
</button>
))}
</div>
</div>
</div>
<Input
label="Username / Handle"
value={accountName}
onChange={setAccountName}
placeholder={platform === "twitter" ? "elonmusk" : "your-username"}
/>
<Input
label="Display Name (optional)"
value={displayName}
onChange={setDisplayName}
placeholder="Your display name"
/>
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<label className="flex items-center gap-3">
<input
@@ -92,6 +78,10 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
</div>
</label>
</div>
<div className="rounded-lg bg-accent px-4 py-3 text-[11px] text-muted-foreground">
A browser window will open for you to sign in. Your username and profile info will be detected automatically after login.
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
@@ -103,15 +93,10 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
Cancel
</button>
<ActionButton
label="Add Account"
onClick={() => {
if (accountName.trim()) {
onAdd(platform, accountName.trim(), displayName.trim(), requireApproval);
}
}}
icon={Plus}
label="Login"
onClick={() => onAddAndLogin(platform, requireApproval)}
icon={SignIn}
tone="accent"
disabled={!accountName.trim()}
/>
</div>
</motion.div>
+7 -13
View File
@@ -13,13 +13,7 @@ interface SocialPanelProps {
actions: BrowserActionRecord[];
agents: Array<{ id: string; name: string }>;
companyId: string;
onSave: (input: {
companyId: string;
platform: SocialPlatform;
accountName: string;
displayName: string;
requireApproval: boolean;
}) => Promise<void>;
onAddAndLogin: (platform: SocialPlatform, requireApproval: boolean) => Promise<void>;
onDelete: (id: string) => Promise<void>;
onLogin: (accountId: string) => Promise<void>;
onOpenPath: (path: string) => Promise<void>;
@@ -31,8 +25,8 @@ export function SocialAccountsPanel({
accounts,
actions,
agents,
companyId,
onSave,
companyId: _companyId,
onAddAndLogin,
onDelete,
onLogin,
onOpenPath,
@@ -43,10 +37,10 @@ export function SocialAccountsPanel({
const [showAddDialog, setShowAddDialog] = useState(false);
const [activeTab, setActiveTab] = useState<"accounts" | "history">("accounts");
const handleAdd = useCallback(async (platform: SocialPlatform, accountName: string, displayName: string, requireApproval: boolean) => {
await onSave({ companyId, platform, accountName, displayName, requireApproval });
const handleAddAndLogin = useCallback(async (platform: SocialPlatform, requireApproval: boolean) => {
await onAddAndLogin(platform, requireApproval);
setShowAddDialog(false);
}, [companyId, onSave]);
}, [onAddAndLogin]);
return (
<div className="space-y-5">
@@ -151,7 +145,7 @@ export function SocialAccountsPanel({
<AnimatePresence>
{showAddDialog ? (
<AddAccountDialog
onAdd={(platform, accountName, displayName, requireApproval) => void handleAdd(platform, accountName, displayName, requireApproval)}
onAddAndLogin={(platform, requireApproval) => void handleAddAndLogin(platform, requireApproval)}
onClose={() => setShowAddDialog(false)}
/>
) : null}
@@ -1,6 +1,5 @@
import type { ElementType } from "react";
import {
Browser,
Camera,
ChatCircle,
Eye,
@@ -9,14 +8,23 @@ import {
MagnifyingGlass,
PaperPlaneTilt,
UserPlus,
DiscordLogo,
FacebookLogo,
GlobeHemisphereWest,
InstagramLogo,
LinkedinLogo,
PinterestLogo,
RedditLogo,
TelegramLogo,
TiktokLogo,
WhatsappLogo,
XLogo,
YoutubeLogo,
ChatTeardrop,
MonitorPlay,
Notebook,
MusicNote,
Lightbulb,
} from "@phosphor-icons/react";
import type { SocialPlatform } from "@shared/types";
@@ -29,6 +37,17 @@ export const platformConfig: Record<SocialPlatform, { label: string; color: stri
youtube: { label: "YouTube", color: "#FF0000", bgColor: "#FF000014", icon: YoutubeLogo },
tiktok: { label: "TikTok", color: "#111315", bgColor: "#11131514", icon: TiktokLogo },
bluesky: { label: "Bluesky", color: "#0085FF", bgColor: "#0085FF14", icon: GlobeHemisphereWest },
discord: { label: "Discord", color: "#5865F2", bgColor: "#5865F214", icon: DiscordLogo },
threads: { label: "Threads", color: "#000000", bgColor: "#00000014", icon: GlobeHemisphereWest },
pinterest: { label: "Pinterest", color: "#E60023", bgColor: "#E6002314", icon: PinterestLogo },
telegram: { label: "Telegram", color: "#26A5E4", bgColor: "#26A5E414", icon: TelegramLogo },
whatsapp: { label: "WhatsApp", color: "#25D366", bgColor: "#25D36614", icon: WhatsappLogo },
wechat: { label: "微信 WeChat", color: "#07C160", bgColor: "#07C16014", icon: ChatTeardrop },
weibo: { label: "微博 Weibo", color: "#E6162D", bgColor: "#E6162D14", icon: Globe },
xiaohongshu: { label: "小红书 RED", color: "#FE2C55", bgColor: "#FE2C5514", icon: Notebook },
douyin: { label: "抖音 Douyin", color: "#161823", bgColor: "#16182314", icon: MusicNote },
bilibili: { label: "哔哩哔哩 Bilibili", color: "#00A1D6", bgColor: "#00A1D614", icon: MonitorPlay },
zhihu: { label: "知乎 Zhihu", color: "#0066FF", bgColor: "#0066FF14", icon: Lightbulb },
other: { label: "Other", color: "#6B7280", bgColor: "#6B728014", icon: GlobeHemisphereWest },
};
@@ -41,6 +60,4 @@ export const actionTypeLabels: Record<string, { label: string; icon: React.Eleme
screenshot: { label: "Screenshot", icon: Camera },
navigate: { label: "Navigate", icon: Globe },
search: { label: "Search", icon: MagnifyingGlass },
dm: { label: "Direct Message", icon: ChatCircle },
custom_script: { label: "Legacy Action", icon: Browser },
};
+2 -2
View File
@@ -40,13 +40,13 @@ const sidebarNavDefs: NavItemDef[] = [
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
];
const paletteOnlyNavDefs: NavItemDef[] = [
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
{ id: "knowledge", labelKey: "nav.knowledge", icon: BookOpen },
];
export function useAppController() {
-56
View File
@@ -1,56 +0,0 @@
/**
* Company Context Provider
*
* Provides shared company infrastructure to section components, eliminating
* the need to thread 80+ props through SectionContentView. Components can
* import useCompanyContext() instead of receiving props from the root.
*
* This context holds stable references to company data and actions that
* nearly every section needs: companyId, snapshot, refresh, navigation, etc.
*/
import { createContext, useContext, useMemo, type ReactNode } from "react";
import type { CompanyRecord, ProfileSnapshot, SectionId } from "@shared/types";
export interface CompanyContextValue {
companyId: string | null;
currentCompany: CompanyRecord | null;
snapshot: ProfileSnapshot | null;
refresh: () => void;
navigateTo: (section: SectionId, entityId?: string) => void;
setSection: (section: SectionId) => void;
setError: (error: string | null) => void;
openPath: (targetPath: string) => Promise<void>;
}
const CompanyContext = createContext<CompanyContextValue | null>(null);
interface CompanyContextProviderProps {
companyId: string | null;
currentCompany: CompanyRecord | null;
snapshot: ProfileSnapshot | null;
refresh: () => void;
navigateTo: (section: SectionId, entityId?: string) => void;
setSection: (section: SectionId) => void;
setError: (error: string | null) => void;
openPath: (targetPath: string) => Promise<void>;
children: ReactNode;
}
export function CompanyContextProvider({
companyId, currentCompany, snapshot, refresh,
navigateTo, setSection, setError, openPath, children,
}: CompanyContextProviderProps) {
const value = useMemo<CompanyContextValue>(() => ({
companyId, currentCompany, snapshot, refresh,
navigateTo, setSection, setError, openPath,
}), [companyId, currentCompany, snapshot, refresh, navigateTo, setSection, setError, openPath]);
return <CompanyContext.Provider value={value}>{children}</CompanyContext.Provider>;
}
export function useCompanyContext(): CompanyContextValue {
const ctx = useContext(CompanyContext);
if (!ctx) throw new Error("useCompanyContext must be used within CompanyContextProvider");
return ctx;
}
+61 -6
View File
@@ -1,3 +1,5 @@
import { useCallback, useRef, useState } from "react";
interface RovingTabIndexResult {
containerRef: { current: HTMLDivElement | null };
focusedIndex: number;
@@ -6,11 +8,11 @@ interface RovingTabIndexResult {
getTabIndex: (index: number) => 0 | -1;
}
/** Roving tabindex for keyboard nav in lists. Supports Arrow/Home/End/j/k with wrap-around. */
export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
// Use internal mutable state for testability outside React
// In a React component context, this still works correctly because
// the function is called once per render and returns fresh closures
/**
* Core roving-tabindex logic, independent of React.
* Used directly by tests and wrapped by `useRovingTabIndex` for React components.
*/
export function createRovingTabIndex(itemCount: number): RovingTabIndexResult {
let _focusedIndex = 0;
const containerRef = { current: null as HTMLDivElement | null };
@@ -41,7 +43,6 @@ export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
_focusedIndex = nextIndex;
// Attempt to focus the target element in the DOM (no-op in tests)
const target = containerRef.current?.querySelector(
`[data-index="${nextIndex}"]`,
) as HTMLElement | null;
@@ -62,3 +63,57 @@ export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
getTabIndex,
};
}
/** React hook wrapper around roving tabindex. Uses real React state for reactivity in components. */
export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
const [focusedIndex, setFocusedIndexState] = useState(0);
const containerRef = useRef<HTMLDivElement | null>(null);
const focusedRef = useRef(0);
const setFocusedIndex = useCallback((index: number) => {
focusedRef.current = index;
setFocusedIndexState(index);
}, []);
const handleKeyDown = useCallback((event: React.KeyboardEvent | { key: string; preventDefault: () => void }) => {
if (itemCount === 0) return;
let nextIndex = focusedRef.current;
if (event.key === "ArrowDown" || event.key === "j") {
event.preventDefault();
nextIndex = (focusedRef.current + 1) % itemCount;
} else if (event.key === "ArrowUp" || event.key === "k") {
event.preventDefault();
nextIndex = (focusedRef.current - 1 + itemCount) % itemCount;
} else if (event.key === "Home") {
event.preventDefault();
nextIndex = 0;
} else if (event.key === "End") {
event.preventDefault();
nextIndex = itemCount - 1;
} else {
return;
}
focusedRef.current = nextIndex;
setFocusedIndexState(nextIndex);
const target = containerRef.current?.querySelector(
`[data-index="${nextIndex}"]`,
) as HTMLElement | null;
target?.focus();
}, [itemCount]);
const getTabIndex = useCallback((index: number): 0 | -1 => {
return index === focusedRef.current ? 0 : -1;
}, []);
return {
containerRef,
focusedIndex,
setFocusedIndex,
handleKeyDown,
getTabIndex,
};
}
@@ -153,19 +153,21 @@ export function OperationsSections({
actions={browserActions}
agents={companyAgents.map(a => ({ id: a.id, name: a.name }))}
companyId={snapshot.currentCompanyId}
onSave={async (input) => {
onAddAndLogin={async (platform, requireApproval) => {
if (!snapshot?.currentCompanyId) return;
try {
unwrap(await window.agentCompany.saveSocialAccount({
companyId: input.companyId,
platform: input.platform,
accountName: input.accountName,
displayName: input.displayName,
requireApproval: input.requireApproval,
const accountId = unwrap(await window.agentCompany.saveSocialAccount({
companyId: snapshot.currentCompanyId,
platform,
accountName: platform,
displayName: "",
requireApproval,
profileUrl: "",
status: "login_required",
metadataJson: "{}",
}));
await refresh();
unwrap(await window.agentCompany.triggerBrowserLogin({ companyId: snapshot.currentCompanyId, socialAccountId: accountId }));
setError(null);
} catch (error) {
setError(error instanceof Error ? error.message : String(error));