feat(03-01): add skill section type wiring, Zustand slice, i18n, and sidebar nav entry
- Add "skills" to SectionId union in shared types - Add SkillRecord, SkillImportResult, SkillUpdateStatus types to shared/types.ts - Create SkillSlice with skills list, selection, file path, and import modal state - Wire SkillSlice into Zustand store composition - Add "nav.skills" i18n keys for English and Chinese - Add Skills sidebar nav entry with PuzzlePiece icon between Communication and Documents - Add section === "skills" rendering route in App.tsx - Clear skill state on company switch - Add skills case to navigateTo in NavigationSlice Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c1de05a7f1
commit
e8d642941f
9 files changed
+121
-1
No files matched your search
@@ -14,6 +14,7 @@ import {
|
|||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Plus,
|
Plus,
|
||||||
|
PuzzlePiece,
|
||||||
UsersThree,
|
UsersThree,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Trash,
|
Trash,
|
||||||
@@ -75,6 +76,7 @@ import { SocialAccountsPanel } from "./components/social";
|
|||||||
import { AutomationPanel } from "./components/automation";
|
import { AutomationPanel } from "./components/automation";
|
||||||
import { CommunicationHub } from "./components/communication";
|
import { CommunicationHub } from "./components/communication";
|
||||||
import { HiringPipeline } from "./components/hiring";
|
import { HiringPipeline } from "./components/hiring";
|
||||||
|
import { SkillsSection } from "./components/skills";
|
||||||
import { DocumentCenter, KnowledgeBaseManager, MeetingsManager, SprintManager } from "./components/company-operations";
|
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 { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionButton, Select, StatusPill, useFocusTrap, useReducedMotion } from "./components/ui";
|
||||||
import ToastContainer from "./components/Toast";
|
import ToastContainer from "./components/Toast";
|
||||||
@@ -94,6 +96,7 @@ const sidebarNavDefs: NavItemDef[] = [
|
|||||||
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
|
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
|
||||||
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
|
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
|
||||||
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
||||||
|
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
|
||||||
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
||||||
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
|
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
|
||||||
{ id: "social", labelKey: "nav.social", icon: Megaphone },
|
{ id: "social", labelKey: "nav.social", icon: Megaphone },
|
||||||
@@ -181,6 +184,10 @@ export function App() {
|
|||||||
store.setSocialAccounts([]);
|
store.setSocialAccounts([]);
|
||||||
store.setBrowserActions([]);
|
store.setBrowserActions([]);
|
||||||
store.setBadges(null);
|
store.setBadges(null);
|
||||||
|
store.setSkills([]);
|
||||||
|
store.setSelectedSkillId(null);
|
||||||
|
store.setSelectedSkillFilePath("SKILL.md");
|
||||||
|
store.setSkillImportModalOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
@@ -1193,6 +1200,10 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{section === "skills" && currentCompany ? (
|
||||||
|
<SkillsSection companyId={currentCompany.id} agents={companyAgents} onRefresh={refresh} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{section === "automation" && currentCompany ? (
|
{section === "automation" && currentCompany ? (
|
||||||
<AutomationPanel
|
<AutomationPanel
|
||||||
key={currentCompany.id}
|
key={currentCompany.id}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { AgentRecord } from "@shared/types";
|
||||||
|
|
||||||
|
interface SkillsSectionProps {
|
||||||
|
companyId: string;
|
||||||
|
agents: AgentRecord[];
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkillsSection({ companyId: _companyId, agents: _agents, onRefresh: _onRefresh }: SkillsSectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<p className="text-[color:var(--muted)]">Skills section placeholder -- Task 2</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SkillsSection } from "./SkillsSection";
|
||||||
@@ -22,6 +22,7 @@ export const en: Record<string, string> = {
|
|||||||
"nav.costs": "Costs",
|
"nav.costs": "Costs",
|
||||||
"nav.activity": "Activity",
|
"nav.activity": "Activity",
|
||||||
"nav.social": "Social Media",
|
"nav.social": "Social Media",
|
||||||
|
"nav.skills": "Skills",
|
||||||
"nav.connectors": "Connectors",
|
"nav.connectors": "Connectors",
|
||||||
"nav.workspaces": "Workspaces",
|
"nav.workspaces": "Workspaces",
|
||||||
"nav.settings": "Settings",
|
"nav.settings": "Settings",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const zh: Record<string, string> = {
|
|||||||
"nav.costs": "成本",
|
"nav.costs": "成本",
|
||||||
"nav.activity": "活动",
|
"nav.activity": "活动",
|
||||||
"nav.social": "社交媒体",
|
"nav.social": "社交媒体",
|
||||||
|
"nav.skills": "技能",
|
||||||
"nav.connectors": "连接器",
|
"nav.connectors": "连接器",
|
||||||
"nav.workspaces": "工作区",
|
"nav.workspaces": "工作区",
|
||||||
"nav.settings": "设置",
|
"nav.settings": "设置",
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const createNavigationSlice: StateCreator<
|
|||||||
else if (section === "connectors") state.selectedConnectorId = entityId;
|
else if (section === "connectors") state.selectedConnectorId = entityId;
|
||||||
else if (section === "communication")
|
else if (section === "communication")
|
||||||
state.selectedCommunicationEntityId = entityId;
|
state.selectedCommunicationEntityId = entityId;
|
||||||
|
else if (section === "skills") state.selectedSkillId = entityId;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { StateCreator } from "zustand";
|
||||||
|
import type { AppState } from "../store";
|
||||||
|
import type { SkillRecord } from "@shared/types";
|
||||||
|
|
||||||
|
export interface SkillSlice {
|
||||||
|
skills: SkillRecord[];
|
||||||
|
setSkills: (skills: SkillRecord[]) => void;
|
||||||
|
selectedSkillId: string | null;
|
||||||
|
setSelectedSkillId: (id: string | null) => void;
|
||||||
|
selectedSkillFilePath: string;
|
||||||
|
setSelectedSkillFilePath: (path: string) => void;
|
||||||
|
skillImportModalOpen: boolean;
|
||||||
|
setSkillImportModalOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSkillSlice: StateCreator<
|
||||||
|
AppState,
|
||||||
|
[["zustand/immer", never]],
|
||||||
|
[],
|
||||||
|
SkillSlice
|
||||||
|
> = (set) => ({
|
||||||
|
skills: [],
|
||||||
|
setSkills: (skills) =>
|
||||||
|
set((state) => {
|
||||||
|
state.skills = skills;
|
||||||
|
}),
|
||||||
|
selectedSkillId: null,
|
||||||
|
setSelectedSkillId: (id) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedSkillId = id;
|
||||||
|
}),
|
||||||
|
selectedSkillFilePath: "SKILL.md",
|
||||||
|
setSelectedSkillFilePath: (path) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedSkillFilePath = path;
|
||||||
|
}),
|
||||||
|
skillImportModalOpen: false,
|
||||||
|
setSkillImportModalOpen: (open) =>
|
||||||
|
set((state) => {
|
||||||
|
state.skillImportModalOpen = open;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -16,13 +16,15 @@ import {
|
|||||||
type CommunicationSlice,
|
type CommunicationSlice,
|
||||||
} from "./slices/communication";
|
} from "./slices/communication";
|
||||||
import { createSocialSlice, type SocialSlice } from "./slices/social";
|
import { createSocialSlice, type SocialSlice } from "./slices/social";
|
||||||
|
import { createSkillSlice, type SkillSlice } from "./slices/skill";
|
||||||
|
|
||||||
export type AppState = NavigationSlice &
|
export type AppState = NavigationSlice &
|
||||||
CompanySlice &
|
CompanySlice &
|
||||||
SelectionSlice &
|
SelectionSlice &
|
||||||
UiSlice &
|
UiSlice &
|
||||||
CommunicationSlice &
|
CommunicationSlice &
|
||||||
SocialSlice;
|
SocialSlice &
|
||||||
|
SkillSlice;
|
||||||
|
|
||||||
export const useAppStore = create<AppState>()(
|
export const useAppStore = create<AppState>()(
|
||||||
devtools(
|
devtools(
|
||||||
@@ -33,6 +35,7 @@ export const useAppStore = create<AppState>()(
|
|||||||
...createUiSlice(...a),
|
...createUiSlice(...a),
|
||||||
...createCommunicationSlice(...a),
|
...createCommunicationSlice(...a),
|
||||||
...createSocialSlice(...a),
|
...createSocialSlice(...a),
|
||||||
|
...createSkillSlice(...a),
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "AgentCompany",
|
name: "AgentCompany",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type SectionId =
|
|||||||
| "activity"
|
| "activity"
|
||||||
| "workspaces"
|
| "workspaces"
|
||||||
| "social"
|
| "social"
|
||||||
|
| "skills"
|
||||||
| "automation"
|
| "automation"
|
||||||
| "communication"
|
| "communication"
|
||||||
| "documents"
|
| "documents"
|
||||||
@@ -453,6 +454,50 @@ export interface OnboardingBootstrapResult {
|
|||||||
runId: string | null;
|
runId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Skill types ──
|
||||||
|
|
||||||
|
export type SkillSourceType = "github" | "local_path" | "url" | "skills_sh" | "managed";
|
||||||
|
export type TrustLevel = "markdown_only" | "assets" | "scripts_executables";
|
||||||
|
|
||||||
|
export interface FileInventoryEntry {
|
||||||
|
path: string;
|
||||||
|
kind: "skill" | "reference" | "script" | "asset" | "markdown" | "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillRecord {
|
||||||
|
id: string;
|
||||||
|
companyId: string;
|
||||||
|
key: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
markdown: string;
|
||||||
|
sourceType: SkillSourceType;
|
||||||
|
sourceLocator: string | null;
|
||||||
|
sourceRef: string | null;
|
||||||
|
trustLevel: TrustLevel;
|
||||||
|
compatibility: string;
|
||||||
|
fileInventory: FileInventoryEntry[];
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillImportResult {
|
||||||
|
skillId: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
isUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillUpdateStatus {
|
||||||
|
skillId: string;
|
||||||
|
key: string;
|
||||||
|
currentRef: string | null;
|
||||||
|
latestRef: string | null;
|
||||||
|
hasUpdate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type SocialPlatform = "twitter" | "linkedin" | "instagram" | "facebook" | "reddit" | "youtube" | "tiktok" | "bluesky" | "other";
|
export type SocialPlatform = "twitter" | "linkedin" | "instagram" | "facebook" | "reddit" | "youtube" | "tiktok" | "bluesky" | "other";
|
||||||
export type SocialAccountStatus = "active" | "logged_in" | "login_required" | "suspended" | "paused";
|
export type SocialAccountStatus = "active" | "logged_in" | "login_required" | "suspended" | "paused";
|
||||||
export type BrowserActionType = "post" | "reply" | "like" | "follow" | "browse_feed" | "screenshot" | "navigate" | "search";
|
export type BrowserActionType = "post" | "reply" | "like" | "follow" | "browse_feed" | "screenshot" | "navigate" | "search";
|
||||||
|
|||||||
Reference in new issue
Block a user