chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes
Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool): - server/hub/ holds the hub-only code (devices, remote sessions, upload ingest, LAN gateway), required only in server mode; hub-boundary.test.js keeps local code from importing it. - A local install enrolls with `agenthub remote login` or Settings → AI tools → Company hub; the "Company account" AI Proxy (reserved name agenthub.company) sends a session through the hub's gateway, which adds a pool account's token — no credential reaches the machine. Only those sessions are uploaded, hourly and at their end; secret-shaped strings are redacted on the client. - Gateway refuses: unknown device (401), a message without a session id, an unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30. - New alert rule type remote_unmonitored: a company-account session its device never registered; a late registration clears the flag instead. - The hub imports a session once per upload round (chunks carry `more`), not once per chunk. Artifacts: - Share an artifact with chosen people (artifact_shares, PATCH sharedWith, GET /api/artifacts/people) besides everyone; a "Shared with you" tab. Only the owner or an admin sees or edits who it is shared with; audited. - Folders: each account's own arrangement (agenthub.artifacts.folders pref). - A claude.ai page keeps a copy viewable without the publishing account: its HTML file (and supporting files) snapshotted at publish, or the copy Claude saves for a Doc / Slides / Design in artifacts-export/ (a synchronous PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch), attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF). Fixes: - tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's tmux (stop the last pane, start again within 10 s failed with ENOENT). - shelves: a shelf restores beside changes in other files. - chat: a finished reply ends the turn even with no hook at all. - Settings → AI tools: profile list display tweaks.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @file PdfView.tsx
|
||||
* @description A PDF drawn by pdf.js onto canvases, page after page — the
|
||||
* Artifacts preview's PDF viewer. The browser's own viewer cannot run inside
|
||||
* the sandbox the preview keeps (it blocks plugins), and opening a shared PDF
|
||||
* on the dashboard's origin would be the risk the sandbox exists for; pdf.js
|
||||
* only DRAWS the file (no scripting sandbox loaded, no XFA, no links
|
||||
* followed), so nothing in it runs. pdf.js and its worker are loaded the
|
||||
* first time a PDF is opened (a separate chunk), never with the dashboard.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
/** Pages drawn at most: a 500-page export would otherwise hold every canvas. */
|
||||
const MAX_PAGES = 200;
|
||||
const MAX_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
async function loadPdfJs() {
|
||||
// The legacy build: the modern one needs `Promise.try` and friends, which a
|
||||
// browser a year old lacks — the PDF would then just fail to open.
|
||||
const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
||||
if (!pdfjs.GlobalWorkerOptions.workerSrc) {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/legacy/build/pdf.worker.min.mjs",
|
||||
import.meta.url
|
||||
).toString();
|
||||
}
|
||||
return pdfjs;
|
||||
}
|
||||
|
||||
export function PdfView({ url, title }: { url: string; title: string }) {
|
||||
const { t } = useTranslation("artifacts");
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const [state, setState] = useState<"loading" | "ready" | "failed">("loading");
|
||||
const [pages, setPages] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
let destroy: (() => void) | null = null;
|
||||
setState("loading");
|
||||
(async () => {
|
||||
const res = await fetch(url, { credentials: "same-origin" });
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const data = new Uint8Array(await res.arrayBuffer());
|
||||
if (data.byteLength > MAX_BYTES) throw new Error("too big");
|
||||
const pdfjs = await loadPdfJs();
|
||||
const task = pdfjs.getDocument({
|
||||
data,
|
||||
enableXfa: false,
|
||||
disableAutoFetch: true,
|
||||
});
|
||||
destroy = () => void task.destroy();
|
||||
const doc = await task.promise;
|
||||
if (!live || !box.current) return;
|
||||
setPages(doc.numPages);
|
||||
const width = Math.max(320, box.current.clientWidth - 32);
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
box.current.replaceChildren();
|
||||
for (let n = 1; n <= Math.min(doc.numPages, MAX_PAGES); n++) {
|
||||
const page = await doc.getPage(n);
|
||||
if (!live || !box.current) return;
|
||||
const base = page.getViewport({ scale: 1 });
|
||||
const viewport = page.getViewport({ scale: (width / base.width) * ratio });
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.floor(viewport.width);
|
||||
canvas.height = Math.floor(viewport.height);
|
||||
canvas.style.width = `${Math.floor(viewport.width / ratio)}px`;
|
||||
canvas.className = "mx-auto mb-3 block bg-white shadow";
|
||||
canvas.setAttribute("aria-label", t("pdf.page", { n }));
|
||||
box.current.appendChild(canvas);
|
||||
await page.render({ canvas, viewport }).promise;
|
||||
if (n === 1 && live) setState("ready");
|
||||
}
|
||||
})().catch(() => live && setState("failed"));
|
||||
return () => {
|
||||
live = false;
|
||||
destroy?.();
|
||||
};
|
||||
}, [url, t]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-auto bg-surface-2 p-4" data-testid="pdf-view">
|
||||
{state === "loading" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-fg-muted">
|
||||
<Loader2 className="h-5 w-5 animate-spin" aria-label={t("loading")} />
|
||||
</div>
|
||||
)}
|
||||
{state === "failed" && (
|
||||
<p className="p-6 text-center text-sm text-fg-secondary">{t("pdf.failed")}</p>
|
||||
)}
|
||||
<div ref={box} role="document" aria-label={title} />
|
||||
{pages > MAX_PAGES && (
|
||||
<p className="py-2 text-center text-xs text-fg-muted">
|
||||
{t("pdf.more", { shown: MAX_PAGES, total: pages })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user