Download web/src/app/chat/components/skill-document-browser.tsx from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 3.26 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/app/chat/components/skill-document-browser.tsx
- Command line
-
hf download hf://spaces/zlcy/study/web/src/app/chat/components/skill-document-browser.tsx
-
curl -L -o skill-document-browser.tsx https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/app/chat/components/skill-document-browser.tsx
3.26 kB
| "use client"; | |
| import { useEffect, useMemo, useState } from "react"; | |
| import { FileText, LoaderCircle } from "lucide-react"; | |
| import { AssistantMarkdown } from "@/components/chat/assistant-markdown"; | |
| import { readSkillResource, type SkillRuntime } from "@/lib/api"; | |
| import { cn } from "@/lib/utils"; | |
| const DOCUMENT_EXTENSIONS = /\.(?:md|mdx|txt|json|jsonc|ya?ml|toml|xml|html?|css|csv|tsv|ini|cfg|conf|py|js|jsx|ts|tsx|sh|ps1)$/i; | |
| function documentPaths(runtime: SkillRuntime) { | |
| return runtime.resources | |
| .filter((resource) => resource.readable && DOCUMENT_EXTENSIONS.test(resource.path)) | |
| .map((resource) => resource.path) | |
| .sort((left, right) => { | |
| if (left === "SKILL.md") return -1; | |
| if (right === "SKILL.md") return 1; | |
| return left.localeCompare(right); | |
| }); | |
| } | |
| export function SkillDocumentBrowser({ skillId, runtime }: { skillId: string; runtime: SkillRuntime }) { | |
| const paths = useMemo(() => documentPaths(runtime), [runtime]); | |
| const [activePath, setActivePath] = useState(paths[0] || ""); | |
| const [documents, setDocuments] = useState<Record<string, string>>({}); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(""); | |
| useEffect(() => { | |
| setActivePath(paths[0] || ""); | |
| setDocuments({}); | |
| setError(""); | |
| }, [skillId, paths]); | |
| useEffect(() => { | |
| if (!activePath || documents[activePath]) return; | |
| let cancelled = false; | |
| setLoading(true); | |
| setError(""); | |
| void readSkillResource(skillId, activePath) | |
| .then((resource) => { | |
| if (!cancelled) setDocuments((current) => ({ ...current, [activePath]: resource.content })); | |
| }) | |
| .catch((reason) => { | |
| if (!cancelled) setError(reason instanceof Error ? reason.message : "读取文档失败"); | |
| }) | |
| .finally(() => { | |
| if (!cancelled) setLoading(false); | |
| }); | |
| return () => { cancelled = true; }; | |
| }, [activePath, documents, skillId]); | |
| if (!paths.length) return <p className="text-xs text-stone-500">这个 Skill 没有可展示的文本文件。</p>; | |
| return ( | |
| <div className="grid min-h-72 overflow-hidden rounded-xl border border-stone-200 bg-white sm:grid-cols-[220px_minmax(0,1fr)]"> | |
| <nav className="max-h-80 overflow-y-auto border-b border-stone-200 bg-stone-50 p-2 sm:border-b-0 sm:border-r" aria-label="Skill 文档列表"> | |
| {paths.map((path) => ( | |
| <button key={path} type="button" onClick={() => setActivePath(path)} className={cn("flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-xs", activePath === path ? "bg-white font-medium text-blue-700 shadow-sm" : "text-stone-600 hover:bg-white")}> | |
| <FileText className="size-3.5 shrink-0" /><span className="truncate" title={path}>{path}</span> | |
| </button> | |
| ))} | |
| </nav> | |
| <div className="max-h-80 min-w-0 overflow-auto p-4"> | |
| {loading && !documents[activePath] ? <div className="flex items-center gap-2 text-sm text-stone-500"><LoaderCircle className="size-4 animate-spin" />正在读取 {activePath}</div> : null} | |
| {error ? <div className="text-sm text-red-600">{error}</div> : null} | |
| {documents[activePath] ? <AssistantMarkdown content={documents[activePath]} /> : null} | |
| </div> | |
| </div> | |
| ); | |
| } | |