- 新增 project_notes 数据层(T5):DB 迁移 + get/add 命令 - MCP 新增 append_project_note 工具,含 group 归属校验(T6) - MentorPanel 底部展示 AI 笔记区域(T7) - 后端全组巡检命令 get_group_mentor_report(T8) - ManagePage 产品组卡片新增「巡检全组」按钮(T9) - 启动健康扫描 + App.tsx 顶部告警 Banner(T10) - 修复 WSL 项目路径检测:wsl_path 为 UNC 格式时 用 unc_to_distro_and_linux() 转换后再调用 wsl 子命令
236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
import { useState } from "react";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { getMentorContext, getProjectNotes, type MentorContext, type ProjectNote } from "../../lib/commands";
|
||
|
||
interface Props {
|
||
projectId: string;
|
||
projectName: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function MentorPanel({ projectId, projectName, onClose }: Props) {
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
const { data: ctx, isLoading, isError, error } = useQuery<MentorContext>({
|
||
queryKey: ["mentorContext", projectId],
|
||
queryFn: () => getMentorContext(projectId),
|
||
staleTime: 30000,
|
||
retry: false,
|
||
});
|
||
|
||
const { data: notes = [] } = useQuery<ProjectNote[]>({
|
||
queryKey: ["projectNotes", projectId],
|
||
queryFn: () => getProjectNotes(projectId),
|
||
staleTime: 30000,
|
||
retry: false,
|
||
});
|
||
|
||
const handleCopy = () => {
|
||
if (!ctx) return;
|
||
navigator.clipboard.writeText(ctx.mentor_markdown).catch(() => {});
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col mx-4">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 shrink-0">
|
||
<div>
|
||
<h2 className="text-base font-semibold text-gray-900">项目导师</h2>
|
||
<p className="text-xs text-gray-400 mt-0.5">{projectName}</p>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-400 hover:text-gray-600 transition-colors p-1"
|
||
aria-label="关闭"
|
||
>
|
||
<svg className="w-5 h-5" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="flex-1 overflow-y-auto min-h-0 px-6 py-5">
|
||
{isLoading && (
|
||
<div className="flex items-center justify-center py-16 text-gray-400 text-sm">
|
||
加载中…
|
||
</div>
|
||
)}
|
||
|
||
{isError && (
|
||
<div className="flex items-center gap-2 px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
||
<span>❌</span>
|
||
<span>{String(error)}</span>
|
||
</div>
|
||
)}
|
||
|
||
{ctx && (
|
||
<div className="flex flex-col gap-5">
|
||
{/* 本地健康 */}
|
||
<section>
|
||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">本地健康</h3>
|
||
<div className="flex flex-col gap-1.5">
|
||
<HealthRow
|
||
ok={ctx.health.path_valid}
|
||
label="本地路径"
|
||
ok_text="有效"
|
||
fail_text="路径不存在"
|
||
/>
|
||
<HealthRow
|
||
ok={ctx.health.has_git}
|
||
label="Git 仓库"
|
||
ok_text="已初始化"
|
||
fail_text="未找到 .git 目录"
|
||
warn={!ctx.health.has_git}
|
||
/>
|
||
{ctx.health.tech_stack && (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<span className="text-gray-400 w-16 shrink-0">技术栈</span>
|
||
<span className="text-gray-700">{ctx.health.tech_stack}</span>
|
||
</div>
|
||
)}
|
||
{ctx.health.last_commit && (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<span className="text-gray-400 w-16 shrink-0">最近提交</span>
|
||
<code className="text-gray-700 font-mono text-[11px] bg-gray-50 px-1.5 py-0.5 rounded">
|
||
{ctx.health.last_commit}
|
||
</code>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 蓝图状态 */}
|
||
<section>
|
||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">蓝图状态</h3>
|
||
{ctx.blueprint_summary ? (
|
||
ctx.blueprint_summary.module_count === 0 ? (
|
||
<p className="text-xs text-gray-400">蓝图存在,但暂无模块数据。</p>
|
||
) : (
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{[
|
||
{ label: "模块", value: `${ctx.blueprint_summary.done_count}/${ctx.blueprint_summary.module_count}` },
|
||
{ label: "任务", value: `${ctx.blueprint_summary.done_tasks}/${ctx.blueprint_summary.total_tasks}` },
|
||
{ label: "可派发", value: String(ctx.blueprint_summary.dispatchable) },
|
||
{ label: "进行中", value: String(ctx.blueprint_summary.in_progress) },
|
||
{ label: "已规划", value: String(ctx.blueprint_summary.planned) },
|
||
].map(({ label, value }) => (
|
||
<div key={label} className="bg-gray-50 rounded-lg px-3 py-2 flex flex-col gap-0.5">
|
||
<span className="text-[11px] text-gray-400">{label}</span>
|
||
<span className="text-sm font-semibold text-gray-800">{value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
) : (
|
||
<p className="text-xs text-gray-400">该项目尚未接入蓝图(无 .blueprint/manifest.yaml)。</p>
|
||
)}
|
||
</section>
|
||
|
||
{/* 近期错误日志 */}
|
||
<section>
|
||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">近期错误日志</h3>
|
||
{ctx.recent_errors.length === 0 ? (
|
||
<p className="text-xs text-gray-400">无错误记录 ✅</p>
|
||
) : (
|
||
<ul className="flex flex-col gap-1 max-h-40 overflow-y-auto">
|
||
{ctx.recent_errors.map((e, i) => (
|
||
<li key={i} className="flex gap-2 text-[11px]">
|
||
<span className={`shrink-0 font-semibold ${e.level === "error" ? "text-red-500" : "text-yellow-500"}`}>
|
||
{e.level === "error" ? "❌" : "⚠️"}
|
||
</span>
|
||
<span className="text-gray-400 shrink-0 font-mono">{e.ts.slice(0, 19)}</span>
|
||
<span className="text-gray-500 shrink-0">[{e.category}]</span>
|
||
<span className="text-gray-700 break-all">{e.message}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
|
||
{/* AI 笔记 */}
|
||
<section>
|
||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">AI 笔记</h3>
|
||
{notes.length === 0 ? (
|
||
<p className="text-xs text-gray-400">暂无笔记。Claude 通过 MCP 分析项目后会在此留下洞察。</p>
|
||
) : (
|
||
<ul className="flex flex-col gap-2 max-h-48 overflow-y-auto">
|
||
{notes.map((note) => (
|
||
<li key={note.id} className="flex flex-col gap-1 rounded-lg bg-indigo-50 border border-indigo-100 px-3 py-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[10px] font-medium text-indigo-400 uppercase tracking-wide">
|
||
{note.source === "mcp" ? "Claude · MCP" : "手动"}
|
||
</span>
|
||
<span className="text-[10px] text-gray-400 font-mono">{note.created_at.slice(0, 16)}</span>
|
||
</div>
|
||
<p className="text-xs text-gray-700 whitespace-pre-wrap leading-relaxed">{note.content}</p>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
{ctx && (
|
||
<div className="px-6 py-4 border-t border-gray-100 shrink-0 flex items-center justify-between">
|
||
<p className="text-xs text-gray-400">生成导师报告后粘贴给 Claude,即可获得质量分析和成长建议</p>
|
||
<button
|
||
onClick={handleCopy}
|
||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-indigo-600 text-white text-xs font-medium hover:bg-indigo-700 transition-colors shrink-0"
|
||
>
|
||
{copied ? (
|
||
<>
|
||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
||
</svg>
|
||
已复制
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/>
|
||
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/>
|
||
</svg>
|
||
生成导师报告
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── 辅助组件 ──────────────────────────────────────────────────────────────────
|
||
|
||
function HealthRow({
|
||
ok,
|
||
label,
|
||
ok_text,
|
||
fail_text,
|
||
warn,
|
||
}: {
|
||
ok: boolean;
|
||
label: string;
|
||
ok_text: string;
|
||
fail_text: string;
|
||
warn?: boolean;
|
||
}) {
|
||
const icon = ok ? "✅" : warn ? "⚠️" : "❌";
|
||
const textColor = ok ? "text-green-700" : warn ? "text-yellow-700" : "text-red-700";
|
||
return (
|
||
<div className="flex items-center gap-2 text-xs">
|
||
<span className="text-gray-400 w-16 shrink-0">{label}</span>
|
||
<span>{icon}</span>
|
||
<span className={textColor}>{ok ? ok_text : fail_text}</span>
|
||
</div>
|
||
);
|
||
}
|