/**
* Prompt Component
*
* Provides a UI for constructing and sending passthrough API requests
* to various LLM providers. Features include:
* - LLM provider and template selection via dropdowns
* - Display of the resolved target API endpoint
* - Monaco-based JSON editor for composing request payloads
* - Editor toolbar for common actions
* - SEND button that POSTs the request and displays the response
*
* CSRF token validation is performed via cookie lookup before each request.
* The API response is rendered by the Response component below the editor.
*
* Dependencies:
* - @monaco-editor/react — code editor
* - @/components/Toolbar, LLMProviderSelector, TemplateSelector, Response — UI components
* - @/lib/cookie, @/lib/django — CSRF and fetch utilities
* - ./templates, ./llmApis — request template and URL helpers
*/
import { useEffect, useState } from "react";
import type * as monaco from "monaco-editor";
import { loggerPrefix } from "@/const";
import getPromptTemplate from "./templates";
import LLMProviderMetaData from "@/components/LLMProviderMetaData";
import LLMProviders, { type LLMProvider } from "@/components/LLMProviders";
import LLMProviderPassthroughResponse from "@/components/LLMProviderPassthroughResponse";
import LLMProviderPassthroughRequest from "@/components/LLMProviderPassthroughRequest";
import fetchDjangoUrl from "@/lib/django";
import "./styles.css";
interface PromptProps {
apiUrl: string;
csrfCookieName: string;
djangoSessionCookieName: string;
cookieDomain: string;
defaultLLMProviderId: string;
defaultTemplateId: string;
providerApiUrl: string;
}
function Prompt({
apiUrl,
csrfCookieName,
djangoSessionCookieName,
cookieDomain,
defaultLLMProviderId,
defaultTemplateId,
providerApiUrl,
}: PromptProps) {
// UI state
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const [isSending, setIsSending] = useState(false);
const [apiResponse, setApiResponse] = useState<{
status: number;
body: any;
} | null>(null);
// LLM provider and template state
const [providersJson, setProviders] = useState<LLMProvider[]>([]);
const [selectedProviderJson, setSelectedProviderJson] = useState<LLMProvider | null>(null);
const [templateId, setTemplateId] = useState(defaultTemplateId ?? "1");
const [llmProviderId, setLLMProvider] = useState(defaultLLMProviderId ?? "1");
// Derived state, from llmProviderId
const [defaultModel, setDefaultModel] = useState("");
const [providerBaseUrl, setProviderBaseUrl] = useState("");
const [providerSlug, setProviderSlug] = useState("");
const [connectivityTestPath, setConnectivityTestPath] = useState("");
// Final request JSON state (function of providersJson, llmProviderId, templateId, defaultModel)
const [requestJson, setRequestJson] = useState("");
const [activeTab, setActiveTab] = useState<"request" | "response">("request");
useEffect(() => {
const controller = new AbortController();
LLMProviders(providerApiUrl, controller.signal)
.then((providers) => {
// set the provider list, and identify the default provider based on
// the "isDefault" flag (or fallback to first provider if none
// marked as default).
console.debug(loggerPrefix, "Fetched LLM providers from API:", providers);
setProviders(providers);
const default_provider = providers.filter((p) => Boolean(p.isDefault) === true)[0] || providers[0];
setSelectedProviderJson(default_provider);
if (!default_provider) {
console.warn(loggerPrefix, "No LLM providers found from API");
return;
}
// initialize all state that depends on the provider list
// and default provider.
setDefaultModel(default_provider.defaultModel);
setLLMProvider(default_provider.id.toString());
// lastly, generate the initial request JSON based on the default
// provider and template.
const templateJson = getPromptTemplate(templateId, default_provider.defaultModel);
setRequestJson(templateJson);
})
.catch((err: Error) => {
if (err.name !== "AbortError") {
console.error(loggerPrefix, "Error fetching LLM providers:", err);
}
});
return () => controller.abort();
}, [providerApiUrl]);
useEffect(() => {
const provider = providersJson.find((p) => String(p.id) === llmProviderId);
if (provider) {
setSelectedProviderJson(provider);
setProviderBaseUrl(provider.baseUrl);
setProviderSlug(provider.rfc1034CompliantName);
setConnectivityTestPath(provider.connectivityTestPath);
setDefaultModel(provider.defaultModel);
}
}, [providersJson, llmProviderId]);
const handleEditorDidMount = (editorInstance: monaco.editor.IStandaloneCodeEditor) => {
setEditor(editorInstance);
};
const handleLLMProviderChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newId = e.target.value;
setLLMProvider(newId);
const provider = providersJson.find((p) => String(p.id) === newId);
if (provider) {
setSelectedProviderJson(provider);
setProviderBaseUrl(provider.baseUrl);
setProviderSlug(provider.rfc1034CompliantName);
setConnectivityTestPath(provider.connectivityTestPath);
setDefaultModel(provider.defaultModel);
const templateJson = getPromptTemplate(templateId, provider.defaultModel);
setRequestJson(templateJson);
}
};
const handleTemplateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setTemplateId(e.target.value);
const templateJson = getPromptTemplate(e.target.value, defaultModel ?? "");
setRequestJson(templateJson);
};
const handleSend = async () => {
if (isSending) {
return;
}
setIsSending(true);
try {
const url = new URL(providerSlug + "/", apiUrl).toString();
const res = await fetchDjangoUrl(
requestJson,
url,
djangoSessionCookieName,
csrfCookieName,
cookieDomain,
);
const data = await res.json();
console.debug(loggerPrefix, `fetched response from ${url}:`, data);
setApiResponse({ status: res.status, body: data });
setActiveTab("response");
} finally {
setIsSending(false);
}
};
return (
<>
<div className="row d-flex mb-3">
<div className="col-lg-12">
<h3 className="mt-4 p-4">LLM Provider API Passthrough</h3>
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item" role="presentation">
<button
className={`nav-link ${activeTab === "request" ? "active" : ""}`}
type="button"
onClick={() => setActiveTab("request")}
role="tab"
aria-selected={activeTab === "request"}
>
Request
</button>
</li>
<li className="nav-item" role="presentation">
<button
className={`nav-link ${activeTab === "response" ? "active" : ""}`}
type="button"
onClick={() => setActiveTab("response")}
role="tab"
aria-selected={activeTab === "response"}
>
Response
</button>
</li>
</ul>
{activeTab === "request" && (
<LLMProviderPassthroughRequest
providersJson={providersJson}
llmProviderId={llmProviderId}
connectivityTestPath={connectivityTestPath}
templateId={templateId}
providerBaseUrl={providerBaseUrl}
isSending={isSending}
editor={editor}
requestJson={requestJson}
onLLMProviderChange={handleLLMProviderChange}
onTemplateChange={handleTemplateChange}
onSend={handleSend}
onEditorDidMount={handleEditorDidMount}
onRequestJsonChange={setRequestJson}
/>
)}
{activeTab === "response" && (
<LLMProviderPassthroughResponse apiResponse={apiResponse} isProcessing={isSending} />
)}
</div>
{activeTab === "request" && <LLMProviderMetaData provider={selectedProviderJson} />}
</div>
</>
);
}
export default Prompt;