mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
feat(web): insights scoped to the selected file or folder
The ⋯ menu's Insights opens /insights/<current path>: a folder scopes the treemap/scatter/hot-path/agent-coverage to its subtree, a file to itself; crumb and title show the scope. urlForView now carries targets for insights like it did for history. 45/45 e2e. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbiaaVM2ACxeRi8ySG9ybc
This commit is contained in:
co-authored by
Claude Fable 5
parent
51ce477b07
commit
3698d70f68
@@ -151,3 +151,15 @@ test("folder listing's Full history goes to the subtree feed", async ({ page })
|
||||
const paths = await page.$$eval(".history .hpath", (els) => els.map((e) => e.textContent));
|
||||
for (const p of paths) expect(p).toContain("notes/");
|
||||
});
|
||||
|
||||
test("insights scopes to the selected folder via the ⋯ menu", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
await page.goto(`/${pid}/notes`);
|
||||
await page.click("#more-btn");
|
||||
await page.click("#more-menu .more-item:has-text('Insights')");
|
||||
await page.waitForURL(`/${pid}/insights/notes`);
|
||||
await expect(page.locator(".in-title .in-scope")).toContainText("notes");
|
||||
// Scope note in the subtitle is the stable assertion.
|
||||
await expect(page.locator(".insights .dl-sub")).toContainText("notes and everything in it");
|
||||
});
|
||||
|
||||
@@ -232,6 +232,7 @@ export default function Browser(props: {
|
||||
flatFiles={flatFiles}
|
||||
heatMap={heatMap}
|
||||
devices={devices}
|
||||
scope={route.viewTarget || ""}
|
||||
onOpenFile={openPath}
|
||||
onOpenFolder={openPath}
|
||||
isFolder={isFolderFn}
|
||||
@@ -333,7 +334,7 @@ export default function Browser(props: {
|
||||
) : path ? (
|
||||
<Breadcrumbs path={path} onOpenFolder={openPath} />
|
||||
) : route.view === "insights" ? (
|
||||
"Insights — " + (project?.name ?? "")
|
||||
"Insights — " + (route.viewTarget || project?.name || "")
|
||||
) : route.view === "history" ? (
|
||||
"History — " + historyTitle(route.viewTarget || "", isFolderFn)
|
||||
) : isHome ? (
|
||||
@@ -393,7 +394,7 @@ export default function Browser(props: {
|
||||
{props.canInsights && (
|
||||
<button
|
||||
className="more-item"
|
||||
onClick={() => navigate(urlForView("insights", project?.id))}
|
||||
onClick={() => navigate(urlForView("insights", project?.id, path))}
|
||||
>
|
||||
Insights
|
||||
</button>
|
||||
|
||||
@@ -48,15 +48,29 @@ export function Insights(props: {
|
||||
flatFiles: Node[];
|
||||
heatMap: HeatMap | null;
|
||||
devices: DeviceHeat[] | null;
|
||||
scope?: string; // "" = whole project; a folder scopes to its subtree, a file to itself
|
||||
onOpenFile: (path: string) => void;
|
||||
onOpenFolder: (path: string) => void;
|
||||
isFolder: (path: string) => boolean;
|
||||
}) {
|
||||
const [lens, setLens] = useState<Lens>("all");
|
||||
const { flatFiles, heatMap, devices } = props;
|
||||
const { flatFiles, heatMap, devices, scope } = props;
|
||||
|
||||
const inScope = (p: string) => !scope || p === scope || p.startsWith(scope + "/");
|
||||
const scoped = scope ? flatFiles.filter((f) => inScope(f.path)) : flatFiles;
|
||||
const scopedDevices =
|
||||
devices && scope
|
||||
? devices
|
||||
.map((d) => {
|
||||
const folders: Record<string, number> = {};
|
||||
for (const [f, n] of Object.entries(d.folders || {})) if (inScope(f)) folders[f] = n;
|
||||
return { ...d, folders };
|
||||
})
|
||||
.filter((d) => Object.keys(d.folders).length > 0)
|
||||
: devices;
|
||||
|
||||
const now = Date.now();
|
||||
const pts: Pt[] = flatFiles.map((f) => {
|
||||
const pts: Pt[] = scoped.map((f) => {
|
||||
const e = (heatMap && heatMap[f.path]) || {};
|
||||
const days = f.time ? Math.max(0, (now - new Date(f.time).getTime()) / 86400000) : 0;
|
||||
const reads = lens === "all" ? heatTotal(e) : e[lens] || 0;
|
||||
@@ -72,10 +86,11 @@ export function Insights(props: {
|
||||
|
||||
return (
|
||||
<div className="insights">
|
||||
<h1 className="in-title">Knowledge insights</h1>
|
||||
<h1 className="in-title">Knowledge insights{scope ? <span className="in-scope"> · {scope}</span> : null}</h1>
|
||||
<p className="dl-sub">
|
||||
Reads over the last 30 days × how long since each file changed. Hot but stale knowledge —
|
||||
read a lot, maintained by nobody — is the danger zone.
|
||||
{scope
|
||||
? `Reads over the last 30 days × freshness, for ${scope} and everything in it.`
|
||||
: "Reads over the last 30 days × how long since each file changed. Hot but stale knowledge — read a lot, maintained by nobody — is the danger zone."}
|
||||
</p>
|
||||
<div className="in-lens">
|
||||
{(["all", "human", "agent"] as const).map((l) => (
|
||||
@@ -98,10 +113,10 @@ export function Insights(props: {
|
||||
<h3 className="dl-h3">Hot path — top files by reads</h3>
|
||||
<HotPath pts={pts} lens={lens} onOpenFile={props.onOpenFile} />
|
||||
|
||||
{devices && devices.length > 0 && (
|
||||
{scopedDevices && scopedDevices.length > 0 && (
|
||||
<>
|
||||
<h3 className="dl-h3">Agent coverage — which agents read which areas</h3>
|
||||
<CoverageMatrix devices={devices} />
|
||||
<CoverageMatrix devices={scopedDevices} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,6 @@ export function urlForView(
|
||||
target?: string,
|
||||
): string {
|
||||
let s = (projectId ? "/" + projectId : "") + "/" + view;
|
||||
if (view === "history" && target) s += "/" + encodePath(target.replace(/\/+$/, ""));
|
||||
if (target) s += "/" + encodePath(target.replace(/\/+$/, ""));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -330,6 +330,7 @@ button, input, a.btn { font-family: inherit; }
|
||||
/* ---- insights (read×write matrix) ---- */
|
||||
.insights { max-width: 760px; margin: 0 auto; }
|
||||
.in-title { font-size: 21px; font-weight: 640; letter-spacing: -.02em; margin: 0 0 4px; color: #f4f6f9; }
|
||||
.in-title .in-scope { color: var(--text-ghost); font-weight: 500; font-size: 15px; }
|
||||
.in-lens { display: flex; gap: 6px; margin: 0 0 14px; }
|
||||
.in-lens-btn { font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: none; color: var(--text-faint); cursor: pointer; }
|
||||
.in-lens-btn:hover { color: var(--text); }
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+8
-8
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BearDrive</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐻</text></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-b5pl3IMV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BWf7Qnlg.css">
|
||||
<script type="module" crossorigin src="/assets/index-BcyLXD3p.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BCO_xByM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user