-
-
{t("docs.title")}
-
{t("docs.subtitle")}
-
-
- {error && (
-
{t("docs.noDocs")}
- )}
-
- {!releases && !error && (
-
- {Array.from({ length: 3 }).map((_, i) => (
-
- ))}
-
- )}
-
- {releases && releases.length === 0 && (
-
{t("docs.noDocs")}
- )}
-
- {releases && releases.length > 0 && (
-
- {releases.map((release) => (
-
-
-
-
-
- ))}
-
- )}
+
+
+
+
{t("docs.title")}
-
+
+ {onSearchChange && (
+
+
+ onSearchChange(e.target.value)}
+ className="pl-8 w-52"
+ data-testid="input-docs-search"
+ />
+
+ )}
+ {versions.length > 0 && (
+
+ )}
+
+
+
);
}
-function DocsDetail() {
+function DocsNav({
+ version,
+ handbook,
+ reference,
+ versions,
+}: {
+ version: string | null;
+ handbook: HandbookPage[] | null;
+ reference: Reference | null;
+ versions: ReleaseDoc[];
+}) {
+ const [location] = useLocation();
const { t } = useTranslation();
- const params = useParams<{ version: string }>();
- const version = params.version;
- const { releases, error: indexError } = useReleases();
- const release = releases?.find((r) => r.version === version);
- const { html, error: mdError } = useReleaseMarkdown(release?.file ?? `${version}.md`);
+
+ const navLink = (href: string) => {
+ const active = location === href || (href !== "/docs" && location.startsWith(href));
+ return active;
+ };
+
+ const groups: { label: string; icon: LucideIcon; items: { href: string; label: string; active: boolean }[] }[] = [];
+
+ if (handbook && handbook.length > 0 && version === null) {
+ groups.push({
+ label: t("docs.guides"),
+ icon: BookOpen,
+ items: handbook.map((p) => ({
+ href: `/docs/handbook/${p.slug}`,
+ label: p.title,
+ active: navLink(`/docs/handbook/${p.slug}`),
+ })),
+ });
+ }
+
+ if (reference) {
+ groups.push({
+ label: t("docs.endpoints"),
+ icon: Server,
+ items: reference.tags.map((tag) => ({
+ href: `/docs/reference/endpoints/${tag.name}`,
+ label: tag.name,
+ active: navLink(`/docs/reference/endpoints/${tag.name}`),
+ })),
+ });
+ groups.push({
+ label: t("docs.schemas"),
+ icon: Library,
+ items: reference.schemas.map((s) => ({
+ href: `/docs/reference/schemas/${s.name}`,
+ label: s.name,
+ active: navLink(`/docs/reference/schemas/${s.name}`),
+ })),
+ });
+ }
+
+ groups.push({
+ label: t("docs.releases"),
+ icon: Tag,
+ items: versions.map((v) => ({
+ href: v.version === (version ?? versions[0]?.version) && version !== null
+ ? `/docs/releases/${v.version}`
+ : `/docs/releases/${v.version}`,
+ label: v.version,
+ active: navLink(`/docs/releases/${v.version}`),
+ })),
+ });
return (
-
-
-
-
- {release && (
-
- {release.version}
-
- )}
+
+ );
+}
- {mdError || (release && !html) ? (
+function Toc({ headings, title }: { headings: Heading[]; title?: string }) {
+ const [activeId, setActiveId] = useState
(null);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ if (headings.length === 0) return;
+ const observer = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) setActiveId(entry.target.id);
+ }
+ },
+ { rootMargin: "-80px 0px -70% 0px" },
+ );
+ for (const h of headings) {
+ const el = document.getElementById(h.id);
+ if (el) observer.observe(el);
+ }
+ return () => observer.disconnect();
+ }, [headings]);
+
+ if (headings.length === 0) return null;
+
+ return (
+
+ );
+}
+
+function MarkdownView({ file }: { file: string | null }) {
+ const { t } = useTranslation();
+ const { html, headings, error } = useMarkdown(file);
+
+ return (
+
+
+ {error ? (
{t("docs.noDocs")}
) : !html ? (
@@ -186,12 +464,490 @@ function DocsDetail() {
/>
)}
-
+
+
);
}
-export default function Docs() {
- const [location] = useLocation();
- const match = location.match(/^\/docs\/(.+)$/);
- return match ?
:
;
+function FieldTypeChip({ type }: { type: FieldType }) {
+ const href = resolveTypeHref(type);
+ const label = fieldTypeLabel(type);
+ if (href) {
+ return (
+
+
+ {label}
+
+
+ );
+ }
+ return
{label};
+}
+
+function FieldTable({ fields }: { fields: Field[] }) {
+ return (
+
+
+
+
+ | Feld |
+ Typ |
+ Pflicht |
+ Beschreibung |
+
+
+
+ {fields.map((f) => (
+
+ |
+
+ {f.name}
+
+ |
+
+
+ |
+
+ {f.required ? (
+ required
+ ) : (
+ –
+ )}
+ |
+
+ {f.description}
+ {f.constraints && (
+ {f.constraints}
+ )}
+ |
+
+ ))}
+
+
+
+ );
+}
+
+function SchemaView({ schema }: { schema: SchemaModel }) {
+ const headings: Heading[] = schema.fields.map((f) => ({ id: f.name, text: f.name, level: 2 }));
+ return (
+
+
+
+
{schema.name}
+ {schema.description &&
{schema.description}
}
+
+
+
+
+ Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle.
+
+
+
+
+ );
+}
+
+function EndpointTagView({ tag }: { tag: TagGroup }) {
+ const headings: Heading[] = tag.endpoints.map((e) => ({
+ id: e.operationId,
+ text: `${e.method} ${e.path}`,
+ level: 2,
+ }));
+ return (
+
+
+
+
{tag.name}
+ {tag.description &&
{tag.description}
}
+
+ {tag.endpoints.map((ep) => (
+
+
+
+ {ep.method}
+
+
{ep.path}
+
+
+
+
+ {ep.summary}
+ {ep.description && {ep.description}
}
+
+ {ep.parameters.length > 0 && (
+
+
Parameter
+
+
+
+
+ | Name |
+ In |
+ Typ |
+ Pflicht |
+ Beschreibung |
+
+
+
+ {ep.parameters.map((p) => (
+
+ | {p.name} |
+ {p.in} |
+ |
+
+ {p.required ? req : "–"}
+ |
+
+ {p.description}
+ {p.constraints && {p.constraints}}
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ {ep.requestBody && (
+
+
+ Request Body {ep.requestBody.required && required}
+
+
+
+ )}
+
+
+
+
+
+ | Status |
+ Beschreibung |
+ Schema |
+
+
+
+ {ep.responses.map((r) => (
+
+ | {r.status} |
+ {r.description} |
+ |
+
+ ))}
+
+
+
+
+ ))}
+
+
+
+ );
+}
+
+function ReleasesView({ versions }: { versions: ReleaseDoc[] }) {
+ const [, setLocation] = useLocation();
+ const { t } = useTranslation();
+ return (
+
+
+
+
{t("docs.releases")}
+
{t("docs.subtitle")}
+
+
+ {versions.map((v) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+function ReleaseNoteView({ version }: { version: string }) {
+ return (
+
+ );
+}
+
+function HandbookView({ slug }: { slug: string }) {
+ const handbookFile = `${slug}.md`;
+ return
;
+}
+
+// ---------------------------------------------------------------------------
+// Search overlay
+// ---------------------------------------------------------------------------
+
+function useDocsSearch(query: string) {
+ const { data, error } = useJson
(query ? `${DOCS_BASE}/search.json` : null);
+ const results = useMemo(() => {
+ if (!query.trim() || !data) return [];
+ const q = query.trim().toLowerCase();
+ return data
+ .filter(
+ (e) =>
+ e.title.toLowerCase().includes(q) ||
+ e.text.toLowerCase().includes(q),
+ )
+ .slice(0, 25);
+ }, [query, data]);
+
+ return { results, error };
+}
+
+function SearchOverlay({ query, onClose }: { query: string; onClose: () => void }) {
+ const { results } = useDocsSearch(query);
+ if (!query.trim()) return null;
+ return (
+
+ {results.length === 0 ? (
+
Keine Treffer
+ ) : (
+ results.map((r) => (
+
+
+ {r.kind === "endpoint" && }
+ {r.kind === "field" && }
+ {r.kind === "guide" && }
+ {r.kind === "release" && }
+
+
+ {r.title}
+ {r.text.slice(0, 80)}
+
+
+ ))
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+
+export default function Docs() {
+ const [location, setLocation] = useLocation();
+ const { t } = useTranslation();
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const { version, path } = useMemo(() => parseDocsPath(location), [location]);
+
+ const { data: versionInfo } = useGetVersion({
+ query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false },
+ });
+
+ const { data: releases, error: releasesError } = useJson(`${DOCS_BASE}/index.json`);
+ const { data: handbook, error: handbookError } = useJson(
+ version === null ? `${DOCS_BASE}/handbook/index.json` : null,
+ );
+
+ const isCurrentVersion =
+ version === null ||
+ (versionInfo?.version && versionInfo.version !== "dev" && version === versionInfo.version) ||
+ (version !== null && (!versionInfo?.version || versionInfo.version === "dev"));
+
+ const refUrl = version === null
+ ? `${DOCS_BASE}/reference.json`
+ : releases?.find((r) => r.version === version)?.hasReference
+ ? `${DOCS_BASE}/versions/${version}.json`
+ : null;
+
+ const { data: reference, error: refError } = useJson(refUrl);
+
+ // Current version detection: prefer running version, fallback newest documented
+ const currentVersion = useMemo(() => {
+ if (releases && releases.length > 0) {
+ if (versionInfo?.version && versionInfo.version !== "dev") {
+ const match = releases.find((r) => r.version === versionInfo.version);
+ if (match) return match.version;
+ }
+ return releases[0].version;
+ }
+ return null;
+ }, [releases, versionInfo]);
+
+ // Redirect old-style /docs/vX.Y.Z to /docs/releases/vX.Y.Z
+ useEffect(() => {
+ if (version !== null && path.length === 0) {
+ setLocation(`/docs/releases/${version}`, { replace: true });
+ }
+ }, [version, path, setLocation]);
+
+ const handleVersionChange = (v: string | null) => {
+ setSearchQuery("");
+ if (v === null || v === currentVersion) {
+ setLocation("/docs");
+ return;
+ }
+ setLocation(`/docs/releases/${v}`);
+ };
+
+ // ---- route resolution ----
+ const section = path[0] ?? "home";
+ const param = path[1];
+
+ let content: React.ReactNode = null;
+
+ if (version !== null && path.length === 0) {
+ content = ;
+ } else if (section === "home") {
+ content =
+ handbook && handbook.length > 0 ? (
+
+ ) : releases && releases.length > 0 ? (
+
+ ) : (
+ {t("docs.noDocs")}
+ );
+ } else if (section === "handbook" && param) {
+ content = ;
+ } else if (section === "reference" && param === "endpoints" && path[2]) {
+ const tag = reference?.tags.find((tg) => tg.name === path[2]);
+ content = tag ? (
+
+ ) : refError || (reference && !tag) ? (
+ {t("docs.noDocs")}
+ ) : (
+
+ );
+ } else if (section === "reference" && param === "schemas" && path[2]) {
+ const schema = reference?.schemas.find((s) => s.name === path[2]);
+ content = schema ? (
+
+ ) : refError || (reference && !schema) ? (
+ {t("docs.noDocs")}
+ ) : (
+
+ );
+ } else if (section === "reference") {
+ content = (
+
+
+
{t("docs.reference")}
+
{t("docs.referenceIntro")}
+ {!reference && !refError &&
}
+ {reference && (
+ <>
+
+
{t("docs.endpoints")}
+
+ {reference.tags.map((tg) => (
+
+ {tg.name}
+
+ {tg.endpoints.length} {t("docs.endpoints")}
+
+
+ ))}
+
+
+
+
{t("docs.schemas")}
+
+ {reference.schemas.map((s) => (
+
+ {s.name}
+
+ ))}
+
+
+ >
+ )}
+
+
+
+ );
+ } else if (section === "releases" && param) {
+ content = ;
+ } else if (section === "releases") {
+ content = releases ? : ;
+ } else {
+ content = {t("docs.noDocs")}
;
+ }
+
+ const showSearch = version === null && section !== "releases";
+
+ return (
+
+
+
+
+ {showSearch &&
setSearchQuery("")} />}
+
+
+
+
+ );
}
diff --git a/artifacts/toolrate/src/pages/tool-detail.tsx b/artifacts/toolrate/src/pages/tool-detail.tsx
index ada05eb..00dc42d 100644
--- a/artifacts/toolrate/src/pages/tool-detail.tsx
+++ b/artifacts/toolrate/src/pages/tool-detail.tsx
@@ -60,6 +60,7 @@ import {
} from "@/components/ui/alert-dialog";
import { customFetch } from "@workspace/api-client-react";
import { recordRecentTool } from "@/lib/recent-tools";
+import { FieldHelp } from "@/components/field-help";
const ratingSchema = z.object({
usefulness: z.number().min(1).max(5),
@@ -776,7 +777,10 @@ export default function ToolDetail() {
name="usefulness"
render={({ field }) => (
- {t("detail.usefulness")}
+
+ {t("detail.usefulness")}
+ {t("detail.usefulness")}
+
(
- {t("detail.usability")}
+
+ {t("detail.usability")}
+ {t("detail.usability")}
+
(
- Comment (Optional)
+
+ Comment (Optional)
+ Comment
+