Added Nginx Reverse Proxy
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
fetchCoursewareRuntimeConfig,
|
||||
normalizeCoursewareRuntimeConfig,
|
||||
type ResolvedCoursewareRuntimeConfig,
|
||||
} from "~/lib/courseware-runtime";
|
||||
|
||||
type PageProps = {
|
||||
params: { service: string };
|
||||
searchParams?: Record<string, string | string[] | undefined>;
|
||||
};
|
||||
|
||||
type ServiceDescriptor = {
|
||||
label: string;
|
||||
resolveUrl: (runtime: ResolvedCoursewareRuntimeConfig) => string | null;
|
||||
allowIframe: boolean;
|
||||
};
|
||||
|
||||
function coerceSearchParam(
|
||||
value: string | string[] | undefined,
|
||||
): string | undefined {
|
||||
if (Array.isArray(value)) return value[0];
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveDescriptor(serviceId: string): ServiceDescriptor | null {
|
||||
switch (serviceId) {
|
||||
case "open-webui":
|
||||
return {
|
||||
label: "Open WebUI",
|
||||
resolveUrl: (runtime) => runtime.services["open-webui"],
|
||||
allowIframe: false,
|
||||
};
|
||||
case "promptfoo":
|
||||
return {
|
||||
label: "Promptfoo",
|
||||
resolveUrl: (runtime) => runtime.services.promptfoo,
|
||||
allowIframe: false,
|
||||
};
|
||||
case "chunkviz":
|
||||
return {
|
||||
label: "ChunkViz",
|
||||
resolveUrl: (runtime) => runtime.services.chunkviz,
|
||||
allowIframe: false,
|
||||
};
|
||||
case "embedding-atlas":
|
||||
return {
|
||||
label: "Embedding Atlas",
|
||||
resolveUrl: (runtime) => runtime.services["embedding-atlas"],
|
||||
allowIframe: false,
|
||||
};
|
||||
case "unsloth":
|
||||
return {
|
||||
label: "Unsloth Studio",
|
||||
resolveUrl: (runtime) => runtime.services.unsloth,
|
||||
allowIframe: false,
|
||||
};
|
||||
case "wetty":
|
||||
return {
|
||||
label: "Lab 3 Terminal",
|
||||
resolveUrl: (runtime) => runtime.lab3TerminalUrl,
|
||||
allowIframe: true,
|
||||
};
|
||||
case "netron":
|
||||
return {
|
||||
label: "Netron",
|
||||
resolveUrl: (runtime) => runtime.lab1NetronUrl,
|
||||
allowIframe: false,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ServiceAppPage({ params, searchParams }: PageProps) {
|
||||
const serviceId = params.service?.trim() ?? "";
|
||||
const descriptor = useMemo(
|
||||
() => resolveDescriptor(serviceId),
|
||||
[serviceId],
|
||||
);
|
||||
const embedParam = coerceSearchParam(searchParams?.embed);
|
||||
const embed = embedParam === "1" || embedParam === "true";
|
||||
|
||||
const [runtime, setRuntime] = useState<ResolvedCoursewareRuntimeConfig>(() =>
|
||||
normalizeCoursewareRuntimeConfig(),
|
||||
);
|
||||
const [isRuntimeResolved, setIsRuntimeResolved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
void fetchCoursewareRuntimeConfig()
|
||||
.then((nextRuntime) => {
|
||||
if (isCancelled) return;
|
||||
setRuntime(nextRuntime);
|
||||
})
|
||||
.catch(() => {
|
||||
if (isCancelled) return;
|
||||
setRuntime(normalizeCoursewareRuntimeConfig());
|
||||
})
|
||||
.finally(() => {
|
||||
if (isCancelled) return;
|
||||
setIsRuntimeResolved(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!descriptor) {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-10">
|
||||
<h1 className="text-2xl font-semibold text-[#004E78]">
|
||||
Unknown tool
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-700">
|
||||
The tool <code>{serviceId}</code> is not configured.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
|
||||
Back to tools
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedUrl = descriptor.resolveUrl(runtime);
|
||||
const shouldEmbed = embed && descriptor.allowIframe;
|
||||
|
||||
if (!isRuntimeResolved) {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-10">
|
||||
<h1 className="text-2xl font-semibold text-[#004E78]">
|
||||
Loading {descriptor.label}…
|
||||
</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!resolvedUrl) {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-10">
|
||||
<h1 className="text-2xl font-semibold text-[#004E78]">
|
||||
{descriptor.label} is unavailable
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-700">
|
||||
The runtime config did not provide a URL for this service.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
|
||||
Back to tools
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldEmbed) {
|
||||
// Full-screen navigation: replace the current page so the URL remains a single origin path.
|
||||
// (Embedding is reserved for services that explicitly allow iframes, like WeTTY.)
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.replace(resolvedUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-10">
|
||||
<h1 className="text-2xl font-semibold text-[#004E78]">
|
||||
Opening {descriptor.label}…
|
||||
</h1>
|
||||
<p className="mt-3 text-slate-700">
|
||||
If you are not redirected automatically, open{" "}
|
||||
<a
|
||||
className="text-[#004E78] underline hover:text-[#F89C27]"
|
||||
href={resolvedUrl}
|
||||
>
|
||||
{resolvedUrl}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
|
||||
Back to tools
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-0px)] w-full">
|
||||
<iframe
|
||||
title={descriptor.label}
|
||||
src={resolvedUrl}
|
||||
className="h-screen w-full border-0"
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const tools = [
|
||||
{ id: "open-webui", label: "Open WebUI" },
|
||||
{ id: "netron", label: "Netron" },
|
||||
{ id: "chunkviz", label: "ChunkViz" },
|
||||
{ id: "embedding-atlas", label: "Embedding Atlas" },
|
||||
{ id: "unsloth", label: "Unsloth Studio" },
|
||||
{ id: "promptfoo", label: "Promptfoo" },
|
||||
{ id: "wetty", label: "Lab 3 Terminal" },
|
||||
] as const;
|
||||
|
||||
export default function AppsIndexPage() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-10">
|
||||
<section className="rounded-xl border border-[#f6d5a5] bg-white p-8 shadow-sm">
|
||||
<h1 className="mb-3 text-3xl font-bold text-[#004E78]">Lab Tools</h1>
|
||||
<p className="max-w-3xl text-slate-700">
|
||||
Launch the courseware services full-screen under the same URL.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 grid gap-4 md:grid-cols-2">
|
||||
{tools.map((tool) => (
|
||||
<Link
|
||||
key={tool.id}
|
||||
href={tool.id === "wetty" ? "/apps/wetty" : `/apps/${tool.id}`}
|
||||
className="block rounded-lg border border-slate-200 bg-white p-5 transition hover:border-[#F89C27] hover:shadow-sm"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-[#004E78]">
|
||||
{tool.label}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
Open full-screen
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export function SiteHeader() {
|
||||
<Link href="/labs" className="hover:text-[#F89C27]">
|
||||
Labs
|
||||
</Link>
|
||||
<Link href="/apps" className="hover:text-[#F89C27]">
|
||||
Tools
|
||||
</Link>
|
||||
<Link
|
||||
href="https://discord.gg/Ma9UZNBxvh"
|
||||
className="rounded-md border border-[#F89C27] px-3 py-1.5 text-[#004E78] hover:bg-[#F89C27] hover:text-white"
|
||||
|
||||
Reference in New Issue
Block a user