feat(webui): render file edit activity
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FileReferenceKind =
|
||||
| "default"
|
||||
| "css"
|
||||
| "html"
|
||||
| "json"
|
||||
| "markdown"
|
||||
| "notebook"
|
||||
| "python"
|
||||
| "react"
|
||||
| "typescript";
|
||||
|
||||
interface FileReferenceChipProps {
|
||||
path: string;
|
||||
display?: "name" | "path";
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function FileReferenceChip({
|
||||
path,
|
||||
display = "name",
|
||||
active = false,
|
||||
className,
|
||||
textClassName,
|
||||
testId = "inline-file-path",
|
||||
}: FileReferenceChipProps) {
|
||||
const { name } = splitFilePath(path);
|
||||
const kind = fileKindForPath(path);
|
||||
const displayText = display === "path" ? path.replace(/\\/g, "/") : name;
|
||||
return (
|
||||
<TooltipProvider delayDuration={500} skipDelayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn("not-prose inline-flex max-w-full align-[0.14em]", className)}
|
||||
>
|
||||
<span
|
||||
data-testid={testId}
|
||||
aria-label={path}
|
||||
className={cn(
|
||||
"inline-flex max-w-full items-center gap-1 font-medium leading-[1.1]",
|
||||
"text-sky-600 transition-colors hover:text-sky-700",
|
||||
"dark:text-sky-300 dark:hover:text-sky-200",
|
||||
)}
|
||||
>
|
||||
<FileReferenceIcon kind={kind} />
|
||||
<span
|
||||
data-sheen-text={active ? displayText : undefined}
|
||||
className={cn(
|
||||
"min-w-0 truncate",
|
||||
active && "streaming-text-sheen",
|
||||
textClassName,
|
||||
)}
|
||||
>
|
||||
{displayText}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="center"
|
||||
sideOffset={8}
|
||||
collisionPadding={12}
|
||||
className={cn(
|
||||
"max-w-[min(38rem,calc(100vw-2rem))] rounded-[10px]",
|
||||
"border-border/60 bg-popover/95 px-2.5 py-1.5",
|
||||
"break-all font-mono text-[11px] leading-snug text-popover-foreground",
|
||||
"shadow-lg backdrop-blur",
|
||||
)}
|
||||
>
|
||||
{path}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function isLikelyFilePath(value: string): boolean {
|
||||
const raw = value.trim();
|
||||
if (!raw || raw.includes("\n")) return false;
|
||||
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return false;
|
||||
if (!/[\\/]/.test(raw) && !/^(dockerfile|makefile|readme|package-lock\.json)$/i.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = raw.replace(/\\/g, "/");
|
||||
const name = normalized.split("/").filter(Boolean).pop() ?? normalized;
|
||||
if (!name || name === "." || name === "..") return false;
|
||||
if (/^(dockerfile|makefile|readme|package-lock\.json)$/i.test(name)) return true;
|
||||
return /\.[a-z0-9][a-z0-9_-]{0,12}$/i.test(name);
|
||||
}
|
||||
|
||||
function splitFilePath(path: string): { directory: string; name: string } {
|
||||
const normalized = path.replace(/\\/g, "/");
|
||||
const slash = normalized.lastIndexOf("/");
|
||||
if (slash < 0) return { directory: "", name: path };
|
||||
return {
|
||||
directory: normalized.slice(0, slash + 1),
|
||||
name: normalized.slice(slash + 1) || normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function fileKindForPath(path: string): FileReferenceKind {
|
||||
const normalized = path.toLowerCase();
|
||||
const name = normalized.split(/[\\/]/).pop() ?? normalized;
|
||||
const ext = name.includes(".") ? name.split(".").pop() ?? "" : "";
|
||||
if (name === "dockerfile") {
|
||||
return "default";
|
||||
}
|
||||
switch (ext) {
|
||||
case "py":
|
||||
case "pyi":
|
||||
return "python";
|
||||
case "jsx":
|
||||
case "tsx":
|
||||
return "react";
|
||||
case "ts":
|
||||
return "typescript";
|
||||
case "html":
|
||||
case "htm":
|
||||
return "html";
|
||||
case "css":
|
||||
case "scss":
|
||||
case "sass":
|
||||
return "css";
|
||||
case "json":
|
||||
case "jsonl":
|
||||
return "json";
|
||||
case "md":
|
||||
case "mdx":
|
||||
return "markdown";
|
||||
case "ipynb":
|
||||
return "notebook";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
function FileReferenceIcon({ kind }: { kind: FileReferenceKind }) {
|
||||
if (kind === "react") {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
className="h-[0.98em] w-[0.98em] shrink-0 text-sky-500 dark:text-sky-300"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1.9" fill="currentColor" stroke="none" />
|
||||
<ellipse cx="12" cy="12" rx="9" ry="3.7" />
|
||||
<ellipse cx="12" cy="12" rx="9" ry="3.7" transform="rotate(60 12 12)" />
|
||||
<ellipse cx="12" cy="12" rx="9" ry="3.7" transform="rotate(120 12 12)" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (kind === "default") {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
className="h-[0.98em] w-[0.98em] shrink-0 text-sky-500 dark:text-sky-300"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.9"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7z" />
|
||||
<path d="M14 2v5h5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
const label = fileKindLabel(kind);
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"inline-flex h-[1.05em] min-w-[1.05em] shrink-0 items-center justify-center",
|
||||
"rounded-[4px] bg-sky-500/12 px-[0.22em] text-[0.58em] font-bold uppercase leading-none",
|
||||
"text-sky-600 dark:bg-sky-400/15 dark:text-sky-300",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function fileKindLabel(kind: FileReferenceKind): string {
|
||||
switch (kind) {
|
||||
case "css":
|
||||
return "#";
|
||||
case "html":
|
||||
return "H";
|
||||
case "json":
|
||||
return "{}";
|
||||
case "markdown":
|
||||
return "M";
|
||||
case "notebook":
|
||||
return "N";
|
||||
case "python":
|
||||
return "PY";
|
||||
case "typescript":
|
||||
return "TS";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user