diff --git a/src/components/agents/AgentsPanel.tsx b/src/components/agents/AgentsPanel.tsx index afa956f..7f53f40 100644 --- a/src/components/agents/AgentsPanel.tsx +++ b/src/components/agents/AgentsPanel.tsx @@ -1,25 +1,22 @@ - import { Bot } from "lucide-react"; interface AgentsPanelProps { - onOpenChange: (open: boolean) => void; + onOpenChange: (open: boolean) => void; } -export function AgentsPanel({ }: AgentsPanelProps) { - return ( -
- - -
-
- -
-

Coming Soon

-

- The Agents management feature is currently under development. - Stay tuned for powerful autonomous capabilities. -

-
+export function AgentsPanel({}: AgentsPanelProps) { + return ( +
+
+
+
- ); +

Coming Soon

+

+ The Agents management feature is currently under development. Stay + tuned for powerful autonomous capabilities. +

+
+
+ ); } diff --git a/src/components/prompts/PromptFormPanel.tsx b/src/components/prompts/PromptFormPanel.tsx index 99d6c1e..fe83615 100644 --- a/src/components/prompts/PromptFormPanel.tsx +++ b/src/components/prompts/PromptFormPanel.tsx @@ -8,141 +8,141 @@ import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import type { Prompt, AppId } from "@/lib/api"; interface PromptFormPanelProps { - appId: AppId; - editingId?: string; - initialData?: Prompt; - onSave: (id: string, prompt: Prompt) => Promise; - onClose: () => void; + appId: AppId; + editingId?: string; + initialData?: Prompt; + onSave: (id: string, prompt: Prompt) => Promise; + onClose: () => void; } const PromptFormPanel: React.FC = ({ - appId, - editingId, - initialData, - onSave, - onClose, + appId, + editingId, + initialData, + onSave, + onClose, }) => { - const { t } = useTranslation(); - const appName = t(`apps.${appId}`); - const filenameMap: Record = { - claude: "CLAUDE.md", - codex: "AGENTS.md", - gemini: "GEMINI.md", - }; - const filename = filenameMap[appId]; - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [content, setContent] = useState(""); - const [saving, setSaving] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(false); + const { t } = useTranslation(); + const appName = t(`apps.${appId}`); + const filenameMap: Record = { + claude: "CLAUDE.md", + codex: "AGENTS.md", + gemini: "GEMINI.md", + }; + const filename = filenameMap[appId]; + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [content, setContent] = useState(""); + const [saving, setSaving] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(false); - useEffect(() => { - setIsDarkMode(document.documentElement.classList.contains("dark")); + useEffect(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); - const observer = new MutationObserver(() => { - setIsDarkMode(document.documentElement.classList.contains("dark")); - }); + const observer = new MutationObserver(() => { + setIsDarkMode(document.documentElement.classList.contains("dark")); + }); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); - return () => observer.disconnect(); - }, []); + return () => observer.disconnect(); + }, []); - useEffect(() => { - if (initialData) { - setName(initialData.name); - setDescription(initialData.description || ""); - setContent(initialData.content); - } - }, [initialData]); + useEffect(() => { + if (initialData) { + setName(initialData.name); + setDescription(initialData.description || ""); + setContent(initialData.content); + } + }, [initialData]); - const handleSave = async () => { - if (!name.trim() || !content.trim()) { - return; - } + const handleSave = async () => { + if (!name.trim() || !content.trim()) { + return; + } - setSaving(true); - try { - const id = editingId || `prompt-${Date.now()}`; - const timestamp = Math.floor(Date.now() / 1000); - const prompt: Prompt = { - id, - name: name.trim(), - description: description.trim() || undefined, - content: content.trim(), - enabled: initialData?.enabled || false, - createdAt: initialData?.createdAt || timestamp, - updatedAt: timestamp, - }; - await onSave(id, prompt); - onClose(); - } catch (error) { - // Error handled by hook - } finally { - setSaving(false); - } - }; + setSaving(true); + try { + const id = editingId || `prompt-${Date.now()}`; + const timestamp = Math.floor(Date.now() / 1000); + const prompt: Prompt = { + id, + name: name.trim(), + description: description.trim() || undefined, + content: content.trim(), + enabled: initialData?.enabled || false, + createdAt: initialData?.createdAt || timestamp, + updatedAt: timestamp, + }; + await onSave(id, prompt); + onClose(); + } catch (error) { + // Error handled by hook + } finally { + setSaving(false); + } + }; - const title = editingId - ? t("prompts.editTitle", { appName }) - : t("prompts.addTitle", { appName }); + const title = editingId + ? t("prompts.editTitle", { appName }) + : t("prompts.addTitle", { appName }); - return ( - +
+ + setName(e.target.value)} + placeholder={t("prompts.namePlaceholder")} + className="mt-2" + /> +
+ +
+ + setDescription(e.target.value)} + placeholder={t("prompts.descriptionPlaceholder")} + className="mt-2" + /> +
+ +
+ + +
+ +
+ -
-
- ); + {saving ? t("common.saving") : t("common.save")} + +
+ + ); }; export default PromptFormPanel; diff --git a/src/components/prompts/PromptPanel.tsx b/src/components/prompts/PromptPanel.tsx index 3b9b987..90e2919 100644 --- a/src/components/prompts/PromptPanel.tsx +++ b/src/components/prompts/PromptPanel.tsx @@ -17,133 +17,138 @@ export interface PromptPanelHandle { openAdd: () => void; } -const PromptPanel = React.forwardRef(({ - open, - appId, -}, ref) => { - const { t } = useTranslation(); - const [isFormOpen, setIsFormOpen] = useState(false); - const [editingId, setEditingId] = useState(null); - const [confirmDialog, setConfirmDialog] = useState<{ - isOpen: boolean; - titleKey: string; - messageKey: string; - messageParams?: Record; - onConfirm: () => void; - } | null>(null); +const PromptPanel = React.forwardRef( + ({ open, appId }, ref) => { + const { t } = useTranslation(); + const [isFormOpen, setIsFormOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [confirmDialog, setConfirmDialog] = useState<{ + isOpen: boolean; + titleKey: string; + messageKey: string; + messageParams?: Record; + onConfirm: () => void; + } | null>(null); - const { prompts, loading, reload, savePrompt, deletePrompt, toggleEnabled } = - usePromptActions(appId); + const { + prompts, + loading, + reload, + savePrompt, + deletePrompt, + toggleEnabled, + } = usePromptActions(appId); - useEffect(() => { - if (open) reload(); - }, [open, reload]); + useEffect(() => { + if (open) reload(); + }, [open, reload]); - const handleAdd = () => { - setEditingId(null); - setIsFormOpen(true); - }; + const handleAdd = () => { + setEditingId(null); + setIsFormOpen(true); + }; - React.useImperativeHandle(ref, () => ({ - openAdd: handleAdd - })); + React.useImperativeHandle(ref, () => ({ + openAdd: handleAdd, + })); - const handleEdit = (id: string) => { - setEditingId(id); - setIsFormOpen(true); - }; + const handleEdit = (id: string) => { + setEditingId(id); + setIsFormOpen(true); + }; - const handleDelete = (id: string) => { - const prompt = prompts[id]; - setConfirmDialog({ - isOpen: true, - titleKey: "prompts.confirm.deleteTitle", - messageKey: "prompts.confirm.deleteMessage", - messageParams: { name: prompt?.name }, - onConfirm: async () => { - try { - await deletePrompt(id); - setConfirmDialog(null); - } catch (e) { - // Error handled by hook - } - }, - }); - }; + const handleDelete = (id: string) => { + const prompt = prompts[id]; + setConfirmDialog({ + isOpen: true, + titleKey: "prompts.confirm.deleteTitle", + messageKey: "prompts.confirm.deleteMessage", + messageParams: { name: prompt?.name }, + onConfirm: async () => { + try { + await deletePrompt(id); + setConfirmDialog(null); + } catch (e) { + // Error handled by hook + } + }, + }); + }; - const promptEntries = useMemo(() => Object.entries(prompts), [prompts]); + const promptEntries = useMemo(() => Object.entries(prompts), [prompts]); - const enabledPrompt = promptEntries.find(([_, p]) => p.enabled); + const enabledPrompt = promptEntries.find(([_, p]) => p.enabled); - return ( -
-
-
- {t("prompts.count", { count: promptEntries.length })} ·{" "} - {enabledPrompt - ? t("prompts.enabledName", { name: enabledPrompt[1].name }) - : t("prompts.noneEnabled")} + return ( +
+
+
+ {t("prompts.count", { count: promptEntries.length })} ·{" "} + {enabledPrompt + ? t("prompts.enabledName", { name: enabledPrompt[1].name }) + : t("prompts.noneEnabled")} +
-
-
- {loading ? ( -
- {t("prompts.loading")} -
- ) : promptEntries.length === 0 ? ( -
-
- +
+ {loading ? ( +
+ {t("prompts.loading")}
-

- {t("prompts.empty")} -

-

- {t("prompts.emptyDescription")} -

-
- ) : ( -
- {promptEntries.map(([id, prompt]) => ( - - ))} -
+ ) : promptEntries.length === 0 ? ( +
+
+ +
+

+ {t("prompts.empty")} +

+

+ {t("prompts.emptyDescription")} +

+
+ ) : ( +
+ {promptEntries.map(([id, prompt]) => ( + + ))} +
+ )} +
+ + {isFormOpen && ( + setIsFormOpen(false)} + /> + )} + + {confirmDialog && ( + setConfirmDialog(null)} + /> )}
- - {isFormOpen && ( - setIsFormOpen(false)} - /> - )} - - {confirmDialog && ( - setConfirmDialog(null)} - /> - )} -
- ); -}); + ); + }, +); PromptPanel.displayName = "PromptPanel"; diff --git a/src/components/skills/RepoManagerPanel.tsx b/src/components/skills/RepoManagerPanel.tsx index a29b554..bb3a7b5 100644 --- a/src/components/skills/RepoManagerPanel.tsx +++ b/src/components/skills/RepoManagerPanel.tsx @@ -9,199 +9,211 @@ import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import type { Skill, SkillRepo } from "@/lib/api/skills"; interface RepoManagerPanelProps { - repos: SkillRepo[]; - skills: Skill[]; - onAdd: (repo: SkillRepo) => Promise; - onRemove: (owner: string, name: string) => Promise; - onClose: () => void; + repos: SkillRepo[]; + skills: Skill[]; + onAdd: (repo: SkillRepo) => Promise; + onRemove: (owner: string, name: string) => Promise; + onClose: () => void; } export function RepoManagerPanel({ - repos, - skills, - onAdd, - onRemove, - onClose, + repos, + skills, + onAdd, + onRemove, + onClose, }: RepoManagerPanelProps) { - const { t } = useTranslation(); - const [repoUrl, setRepoUrl] = useState(""); - const [branch, setBranch] = useState(""); - const [skillsPath, setSkillsPath] = useState(""); - const [error, setError] = useState(""); + const { t } = useTranslation(); + const [repoUrl, setRepoUrl] = useState(""); + const [branch, setBranch] = useState(""); + const [skillsPath, setSkillsPath] = useState(""); + const [error, setError] = useState(""); - const getSkillCount = (repo: SkillRepo) => - skills.filter( - (skill) => - skill.repoOwner === repo.owner && - skill.repoName === repo.name && - (skill.repoBranch || "main") === (repo.branch || "main"), - ).length; + const getSkillCount = (repo: SkillRepo) => + skills.filter( + (skill) => + skill.repoOwner === repo.owner && + skill.repoName === repo.name && + (skill.repoBranch || "main") === (repo.branch || "main"), + ).length; - const parseRepoUrl = ( - url: string, - ): { owner: string; name: string } | null => { - let cleaned = url.trim(); - cleaned = cleaned.replace(/^https?:\/\/github\.com\//, ""); - cleaned = cleaned.replace(/\.git$/, ""); + const parseRepoUrl = ( + url: string, + ): { owner: string; name: string } | null => { + let cleaned = url.trim(); + cleaned = cleaned.replace(/^https?:\/\/github\.com\//, ""); + cleaned = cleaned.replace(/\.git$/, ""); - const parts = cleaned.split("/"); - if (parts.length === 2 && parts[0] && parts[1]) { - return { owner: parts[0], name: parts[1] }; - } + const parts = cleaned.split("/"); + if (parts.length === 2 && parts[0] && parts[1]) { + return { owner: parts[0], name: parts[1] }; + } - return null; - }; + return null; + }; - const handleAdd = async () => { - setError(""); + const handleAdd = async () => { + setError(""); - const parsed = parseRepoUrl(repoUrl); - if (!parsed) { - setError(t("skills.repo.invalidUrl")); - return; - } + const parsed = parseRepoUrl(repoUrl); + if (!parsed) { + setError(t("skills.repo.invalidUrl")); + return; + } - try { - await onAdd({ - owner: parsed.owner, - name: parsed.name, - branch: branch || "main", - enabled: true, - skillsPath: skillsPath.trim() || undefined, - }); + try { + await onAdd({ + owner: parsed.owner, + name: parsed.name, + branch: branch || "main", + enabled: true, + skillsPath: skillsPath.trim() || undefined, + }); - setRepoUrl(""); - setBranch(""); - setSkillsPath(""); - } catch (e) { - setError(e instanceof Error ? e.message : t("skills.repo.addFailed")); - } - }; + setRepoUrl(""); + setBranch(""); + setSkillsPath(""); + } catch (e) { + setError(e instanceof Error ? e.message : t("skills.repo.addFailed")); + } + }; - const handleOpenRepo = async (owner: string, name: string) => { - try { - await settingsApi.openExternal(`https://github.com/${owner}/${name}`); - } catch (error) { - console.error("Failed to open URL:", error); - } - }; + const handleOpenRepo = async (owner: string, name: string) => { + try { + await settingsApi.openExternal(`https://github.com/${owner}/${name}`); + } catch (error) { + console.error("Failed to open URL:", error); + } + }; - return ( - - {/* 添加仓库表单 */} -
-

添加技能仓库

-
-
- - setRepoUrl(e.target.value)} - className="mt-2" - /> -
-
-
- - setBranch(e.target.value)} - className="mt-2" - /> -
-
- - setSkillsPath(e.target.value)} - className="mt-2" - /> -
-
- {error &&

{error}

} - + return ( + + {/* 添加仓库表单 */} +
+

+ 添加技能仓库 +

+
+
+ + setRepoUrl(e.target.value)} + className="mt-2" + /> +
+
+
+ + setBranch(e.target.value)} + className="mt-2" + /> +
+
+ + setSkillsPath(e.target.value)} + className="mt-2" + /> +
+
+ {error && ( +

{error}

+ )} + +
+
+ + {/* 仓库列表 */} +
+

+ {t("skills.repo.list")} +

+ {repos.length === 0 ? ( +
+

+ {t("skills.repo.empty")} +

+
+ ) : ( +
+ {repos.map((repo) => ( +
+
+
+ {repo.owner}/{repo.name} +
+
+ {t("skills.repo.branch")}: {repo.branch || "main"} + {repo.skillsPath && ( + <> + + {t("skills.repo.path")}: {repo.skillsPath} + + )} + + {t("skills.repo.skillCount", { + count: getSkillCount(repo), + })} + +
-
- - {/* 仓库列表 */} -
-

{t("skills.repo.list")}

- {repos.length === 0 ? ( -
-

- {t("skills.repo.empty")} -

-
- ) : ( -
- {repos.map((repo) => ( -
-
-
- {repo.owner}/{repo.name} -
-
- {t("skills.repo.branch")}: {repo.branch || "main"} - {repo.skillsPath && ( - <> - - {t("skills.repo.path")}: {repo.skillsPath} - - )} - - {t("skills.repo.skillCount", { - count: getSkillCount(repo), - })} - -
-
-
- - -
-
- ))} -
- )} -
- - ); +
+ + +
+
+ ))} +
+ )} +
+ + ); } diff --git a/src/components/skills/SkillsPage.tsx b/src/components/skills/SkillsPage.tsx index 497ffa9..3f3f889 100644 --- a/src/components/skills/SkillsPage.tsx +++ b/src/components/skills/SkillsPage.tsx @@ -16,155 +16,160 @@ export interface SkillsPageHandle { openRepoManager: () => void; } -export const SkillsPage = forwardRef(({ onClose: _onClose }, ref) => { - const { t } = useTranslation(); - const [skills, setSkills] = useState([]); - const [repos, setRepos] = useState([]); - const [loading, setLoading] = useState(true); - const [repoManagerOpen, setRepoManagerOpen] = useState(false); +export const SkillsPage = forwardRef( + ({ onClose: _onClose }, ref) => { + const { t } = useTranslation(); + const [skills, setSkills] = useState([]); + const [repos, setRepos] = useState([]); + const [loading, setLoading] = useState(true); + const [repoManagerOpen, setRepoManagerOpen] = useState(false); - const loadSkills = async (afterLoad?: (data: Skill[]) => void) => { - try { - setLoading(true); - const data = await skillsApi.getAll(); - setSkills(data); - if (afterLoad) { - afterLoad(data); + const loadSkills = async (afterLoad?: (data: Skill[]) => void) => { + try { + setLoading(true); + const data = await skillsApi.getAll(); + setSkills(data); + if (afterLoad) { + afterLoad(data); + } + } catch (error) { + toast.error(t("skills.loadFailed"), { + description: + error instanceof Error ? error.message : t("common.error"), + }); + } finally { + setLoading(false); } - } catch (error) { - toast.error(t("skills.loadFailed"), { - description: error instanceof Error ? error.message : t("common.error"), - }); - } finally { - setLoading(false); - } - }; + }; - const loadRepos = async () => { - try { - const data = await skillsApi.getRepos(); - setRepos(data); - } catch (error) { - console.error("Failed to load repos:", error); - } - }; + const loadRepos = async () => { + try { + const data = await skillsApi.getRepos(); + setRepos(data); + } catch (error) { + console.error("Failed to load repos:", error); + } + }; - useEffect(() => { - Promise.all([loadSkills(), loadRepos()]); - }, []); + useEffect(() => { + Promise.all([loadSkills(), loadRepos()]); + }, []); - useImperativeHandle(ref, () => ({ - refresh: () => loadSkills(), - openRepoManager: () => setRepoManagerOpen(true) - })); + useImperativeHandle(ref, () => ({ + refresh: () => loadSkills(), + openRepoManager: () => setRepoManagerOpen(true), + })); - const handleInstall = async (directory: string) => { - try { - await skillsApi.install(directory); - toast.success(t("skills.installSuccess", { name: directory })); - await loadSkills(); - } catch (error) { - toast.error(t("skills.installFailed"), { - description: error instanceof Error ? error.message : t("common.error"), - }); - } - }; + const handleInstall = async (directory: string) => { + try { + await skillsApi.install(directory); + toast.success(t("skills.installSuccess", { name: directory })); + await loadSkills(); + } catch (error) { + toast.error(t("skills.installFailed"), { + description: + error instanceof Error ? error.message : t("common.error"), + }); + } + }; - const handleUninstall = async (directory: string) => { - try { - await skillsApi.uninstall(directory); - toast.success(t("skills.uninstallSuccess", { name: directory })); - await loadSkills(); - } catch (error) { - toast.error(t("skills.uninstallFailed"), { - description: error instanceof Error ? error.message : t("common.error"), - }); - } - }; + const handleUninstall = async (directory: string) => { + try { + await skillsApi.uninstall(directory); + toast.success(t("skills.uninstallSuccess", { name: directory })); + await loadSkills(); + } catch (error) { + toast.error(t("skills.uninstallFailed"), { + description: + error instanceof Error ? error.message : t("common.error"), + }); + } + }; - const handleAddRepo = async (repo: SkillRepo) => { - await skillsApi.addRepo(repo); + const handleAddRepo = async (repo: SkillRepo) => { + await skillsApi.addRepo(repo); - let repoSkillCount = 0; - await Promise.all([ - loadRepos(), - loadSkills((data) => { - repoSkillCount = data.filter( - (skill) => - skill.repoOwner === repo.owner && - skill.repoName === repo.name && - (skill.repoBranch || "main") === (repo.branch || "main"), - ).length; - }), - ]); + let repoSkillCount = 0; + await Promise.all([ + loadRepos(), + loadSkills((data) => { + repoSkillCount = data.filter( + (skill) => + skill.repoOwner === repo.owner && + skill.repoName === repo.name && + (skill.repoBranch || "main") === (repo.branch || "main"), + ).length; + }), + ]); - toast.success( - t("skills.repo.addSuccess", { - owner: repo.owner, - name: repo.name, - count: repoSkillCount, - }), - ); - }; + toast.success( + t("skills.repo.addSuccess", { + owner: repo.owner, + name: repo.name, + count: repoSkillCount, + }), + ); + }; - const handleRemoveRepo = async (owner: string, name: string) => { - await skillsApi.removeRepo(owner, name); - toast.success(t("skills.repo.removeSuccess", { owner, name })); - await Promise.all([loadRepos(), loadSkills()]); - }; + const handleRemoveRepo = async (owner: string, name: string) => { + await skillsApi.removeRepo(owner, name); + toast.success(t("skills.repo.removeSuccess", { owner, name })); + await Promise.all([loadRepos(), loadSkills()]); + }; - return ( -
- {/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */} + return ( +
+ {/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */} - {/* 技能网格(可滚动详情区域) */} -
- {loading ? ( -
- -
- ) : skills.length === 0 ? ( -
-

- {t("skills.empty")} -

-

- {t("skills.emptyDescription")} -

- -
- ) : ( -
- {skills.map((skill) => ( - - ))} -
+ {/* 技能网格(可滚动详情区域) */} +
+ {loading ? ( +
+ +
+ ) : skills.length === 0 ? ( +
+

+ {t("skills.empty")} +

+

+ {t("skills.emptyDescription")} +

+ +
+ ) : ( +
+ {skills.map((skill) => ( + + ))} +
+ )} +
+ + {/* 仓库管理面板 */} + {repoManagerOpen && ( + setRepoManagerOpen(false)} + /> )}
- - {/* 仓库管理面板 */} - {repoManagerOpen && ( - setRepoManagerOpen(false)} - /> - )} -
- ); -}); + ); + }, +); SkillsPage.displayName = "SkillsPage"; diff --git a/src/types.ts b/src/types.ts index b4cb57f..ff0fcbf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,14 @@ export interface UsageScript { accessToken?: string; // 访问令牌(NewAPI 模板使用) userId?: string; // 用户ID(NewAPI 模板使用) autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用) + autoIntervalMinutes?: number; // 自动查询间隔(分钟)- 别名字段 + request?: { + // 请求配置 + url?: string; // 请求 URL + method?: string; // HTTP 方法 + headers?: Record; // 请求头 + body?: any; // 请求体 + }; } // 单个套餐用量数据 @@ -101,6 +109,8 @@ export interface Settings { geminiConfigDir?: string; // 首选语言(可选,默认中文) language?: "en" | "zh"; + // 是否开机自启 + launchOnStartup?: boolean; // Claude 自定义端点列表 customEndpointsClaude?: Record; // Codex 自定义端点列表