mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
feat(ui): restyle Decisions playground after OpenRouter's model playground
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9e2aae8490
commit
8fdc2150da
4 changed files with 139 additions and 270 deletions
|
|
@ -1,33 +0,0 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import QuestionBreakdown from "./QuestionBreakdown";
|
||||
import { validateSystemOnePayload } from "./lib/validatePayload";
|
||||
|
||||
const previewOf = (questions: object) => validateSystemOnePayload(JSON.stringify({ state: "Hi", questions })).payload;
|
||||
|
||||
describe("QuestionBreakdown", () => {
|
||||
it("previews every noul criterion that will be sent, not only true and false", () => {
|
||||
render(
|
||||
<QuestionBreakdown
|
||||
payload={previewOf({
|
||||
escalate: {
|
||||
type: "noul",
|
||||
instructions: "Escalate?",
|
||||
criteria: { true: "Asks for a person", unsure: "Maybe" },
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Asks for a person")).toBeInTheDocument();
|
||||
expect(screen.getByText("unsure")).toBeInTheDocument();
|
||||
expect(screen.getByText("Maybe")).toBeInTheDocument();
|
||||
expect(screen.queryByText("No criteria defined")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("says when a noul question has no criteria", () => {
|
||||
render(<QuestionBreakdown payload={previewOf({ escalate: { type: "noul", instructions: "Escalate?" } })} />);
|
||||
|
||||
expect(screen.getByText("No criteria defined")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { PlaygroundQuestion, PlaygroundRequest } from "./lib/schemas";
|
||||
|
||||
function formatState(state: unknown): string {
|
||||
if (typeof state === "string") {
|
||||
return state;
|
||||
}
|
||||
return JSON.stringify(state, null, 2) ?? String(state);
|
||||
}
|
||||
|
||||
function QuestionCriteria({ question }: { question: PlaygroundQuestion }) {
|
||||
if (question.type === "choice") {
|
||||
return (
|
||||
<dl className="grid gap-2">
|
||||
{Object.entries(question.criteria).map(([label, description]) => (
|
||||
<div key={label} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
|
||||
<dt className="font-mono text-xs font-medium">{label}</dt>
|
||||
<dd className="text-xs text-muted-foreground">{formatState(description)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
if (question.type === "noul") {
|
||||
const criteria = Object.entries(question.criteria ?? {}).filter(([, description]) => description !== undefined);
|
||||
return criteria.length > 0 ? (
|
||||
<dl className="grid gap-2">
|
||||
{criteria.map(([label, description]) => (
|
||||
<div key={label} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
|
||||
<dt className="text-xs font-medium">{label}</dt>
|
||||
<dd className="text-xs text-muted-foreground">
|
||||
{typeof description === "string" ? description : JSON.stringify(description)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">No criteria defined</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ol className="grid gap-2">
|
||||
{question.criteria.map((description, index) => (
|
||||
<li key={`${index}-${description}`} className="grid gap-0.5 sm:grid-cols-[minmax(7rem,auto)_1fr] sm:gap-3">
|
||||
<span className="font-mono text-xs font-medium">{index}</span>
|
||||
<span className="text-xs text-muted-foreground">{formatState(description)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuestionBreakdown({ payload }: { payload?: PlaygroundRequest }) {
|
||||
if (!payload) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Question breakdown</CardTitle>
|
||||
<CardDescription>Enter a valid request to preview its state and questions.</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Question breakdown</CardTitle>
|
||||
<CardDescription>Review the state and criteria that will be sent.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 wrap-anywhere">
|
||||
<section aria-labelledby="system-one-state-heading" className="grid gap-2">
|
||||
<h3 id="system-one-state-heading" className="text-sm font-medium">
|
||||
State
|
||||
</h3>
|
||||
<pre className="max-h-48 overflow-y-auto whitespace-pre-wrap rounded-md bg-muted p-3 font-mono text-xs">
|
||||
{formatState(payload.state)}
|
||||
</pre>
|
||||
</section>
|
||||
<div className="grid gap-3">
|
||||
{Object.entries(payload.questions).map(([id, question]) => (
|
||||
<section key={id} className="grid gap-3 rounded-md border p-3" aria-labelledby={`question-${id}`}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 id={`question-${id}`} className="font-mono text-sm font-medium">
|
||||
{id}
|
||||
</h3>
|
||||
<Badge variant="secondary">{question.type}</Badge>
|
||||
</div>
|
||||
{question.instructions != null && <p className="text-sm">{formatState(question.instructions)}</p>}
|
||||
<div className="grid gap-2">
|
||||
<h4 className="text-xs font-medium text-muted-foreground">Criteria</h4>
|
||||
<QuestionCriteria question={question} />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import type { ReactElement } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import SystemOneUI, { NOUL_HELP } from "./SystemOneUI";
|
||||
import SystemOneUI from "./SystemOneUI";
|
||||
import type { SystemOneResponse } from "./lib/schemas";
|
||||
import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions";
|
||||
|
||||
|
|
@ -194,17 +194,11 @@ describe("SystemOneUI integration", () => {
|
|||
expect(screen.queryByText(/TypeSafe/)).not.toBeInTheDocument();
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
expect(screen.getByRole("note", { name: "Decision endpoint notice" })).toHaveTextContent(
|
||||
"omit model to use the proxy's configured default.",
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "How to call /v1/decisions and /v1/systemone" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://docs.litellm.ai/docs/decisions",
|
||||
);
|
||||
expect(screen.getByRole("link", { name: "Give us feedback on what you want for decision models" })).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/BerriAI/litellm/discussions/44231",
|
||||
);
|
||||
expect(screen.queryByRole("note")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -653,6 +647,27 @@ describe("SystemOneUI integration", () => {
|
|||
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("keeps Send below the form and shows results only after a request", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SystemOneUI accessToken="session-key" />);
|
||||
|
||||
const send = screen.getByRole("button", { name: "Send" });
|
||||
const editor = screen.getByRole("region", { name: "Decisions request editor" });
|
||||
const form = within(editor).getByRole("textbox", { name: "Input" });
|
||||
expect(form.compareDocumentPosition(send) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("note")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(send);
|
||||
|
||||
const results = await screen.findByRole("region", { name: "Decisions results" });
|
||||
expect(within(results).getByText("Calibrated probabilities")).toBeInTheDocument();
|
||||
expect(editor.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
fireEvent.change(within(editor).getByRole("textbox", { name: "Input" }), { target: { value: "Edited state" } });
|
||||
expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends to /v1/systemone even without discovered models and renders calibrated answers", async () => {
|
||||
const user = userEvent.setup();
|
||||
sessionStorage.setItem("customProxyBaseUrl", "https://stale.example.com/");
|
||||
|
|
@ -712,7 +727,8 @@ describe("SystemOneUI integration", () => {
|
|||
|
||||
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Form" }));
|
||||
expect(screen.getByText("Enter a valid request to preview its state and questions.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
|
||||
expect(screen.queryByRole("region", { name: "Decisions results" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends structured native questions without changing their payload", async () => {
|
||||
|
|
@ -729,7 +745,6 @@ describe("SystemOneUI integration", () => {
|
|||
fireEvent.change(editor, { target: { value: draft } });
|
||||
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
|
||||
await user.click(screen.getByRole("tab", { name: "Form" }));
|
||||
expect(screen.getByText(/"text": "Help"/)).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||
expect(await screen.findByText("Selected choice")).toBeInTheDocument();
|
||||
|
|
@ -920,13 +935,10 @@ describe("SystemOneUI integration", () => {
|
|||
fireEvent.blur(field);
|
||||
}
|
||||
|
||||
it("explains noul on hover in the banner and on the answer type option", async () => {
|
||||
it("explains noul on the answer type option", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SystemOneUI accessToken="session-key" />);
|
||||
|
||||
await user.hover(screen.getByText("noul", { selector: "span.cursor-help" }));
|
||||
expect(await screen.findByText(NOUL_HELP)).toBeInTheDocument();
|
||||
|
||||
question("has_repro_steps").getByRole("combobox", { name: "Answer type" }).focus();
|
||||
await user.keyboard("{ArrowDown}");
|
||||
expect(await screen.findByRole("option", { name: "noul" })).toHaveAttribute(
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { SimpleTooltip } from "@/components/ui/tooltip";
|
||||
import { SearchSelect } from "@/components/shared/SearchSelect";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import { DECISIONS_DOCS_URL } from "@/lib/decisionModels";
|
||||
import { uiHref } from "@/utils/uiHref";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Code, Eraser, Info, LoaderCircle, Send } from "lucide-react";
|
||||
import { BookOpen, Code, Eraser, LoaderCircle, Send } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { makePlaygroundDecisionRequest, type PlaygroundDecisionRequest } from "../../llm_calls/system_one";
|
||||
import { DECISION_PRESETS, emptyPayload, matchPreset, presetPayload, type DecisionPreset } from "./lib/example";
|
||||
import { payloadModel, withPayloadModel } from "./lib/payloadModel";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
import QuestionBreakdown from "./QuestionBreakdown";
|
||||
import ResponseView from "./ResponseView";
|
||||
import SystemOneForm from "./SystemOneForm";
|
||||
import OpenAIDecisionsForm from "./OpenAIDecisionsForm";
|
||||
|
|
@ -38,7 +37,6 @@ interface SystemOneSendVariables {
|
|||
|
||||
export const NOUL_HELP = "noul is a yes / no question. The model returns the probability that the answer is yes.";
|
||||
const CUSTOM_PRESET = "custom";
|
||||
const DECISION_MODELS_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/44231";
|
||||
|
||||
interface PresetPickerProps {
|
||||
activePreset: DecisionPreset | undefined;
|
||||
|
|
@ -103,6 +101,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
|
|||
makePlaygroundDecisionRequest(request, apiKey, getCustomProxyBaseUrl(), { signal }),
|
||||
});
|
||||
const isLoading = systemOne.isPending;
|
||||
const hasResult = isLoading || systemOne.data !== undefined || systemOne.error !== null;
|
||||
const { reset: resetSystemOne } = systemOne;
|
||||
|
||||
useEffect(() => () => activeController.current?.abort(), []);
|
||||
|
|
@ -163,49 +162,49 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-4 overflow-auto p-4 xl:overflow-hidden">
|
||||
<section className="grid gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">Endpoint</span>
|
||||
<Select
|
||||
value={endpoint}
|
||||
onValueChange={(value) => {
|
||||
if (value === "/v1/decisions" || value === "/v1/systemone") {
|
||||
clearRequestState();
|
||||
setEndpoint(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-80" aria-label="Decision endpoint">
|
||||
<SelectValue>{endpoint}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="/v1/decisions">/v1/decisions</SelectItem>
|
||||
<SelectItem value="/v1/systemone">/v1/systemone</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="ml-2 text-sm font-medium text-muted-foreground">Model</span>
|
||||
<div className="w-80">
|
||||
<SearchSelect
|
||||
aria-label="Decision model"
|
||||
options={decisionModels.map((model) => ({ label: model, value: model }))}
|
||||
value={payloadModel(rawPayload) ?? null}
|
||||
onValueChange={handleModelPick}
|
||||
placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"}
|
||||
emptyText="No decision models found"
|
||||
disabled={hasSyntaxError}
|
||||
allowClear={false}
|
||||
/>
|
||||
</div>
|
||||
{decisionModelsLoaded && decisionModels.length === 0 && (
|
||||
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
|
||||
Add a decision model
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex h-full min-h-0 flex-col overflow-auto p-4 xl:overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto grid w-full gap-4 xl:min-h-0 xl:flex-1",
|
||||
hasResult ? "max-w-7xl xl:grid-cols-2" : "max-w-3xl",
|
||||
)}
|
||||
>
|
||||
<section className="flex min-h-96 flex-col gap-3 xl:min-h-0" aria-label="Decisions request editor">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">Virtual Key Source</span>
|
||||
<Select
|
||||
value={endpoint}
|
||||
onValueChange={(value) => {
|
||||
if (value === "/v1/decisions" || value === "/v1/systemone") {
|
||||
clearRequestState();
|
||||
setEndpoint(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-44" aria-label="Decision endpoint">
|
||||
<SelectValue>{endpoint}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="/v1/decisions">/v1/decisions</SelectItem>
|
||||
<SelectItem value="/v1/systemone">/v1/systemone</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="w-56">
|
||||
<SearchSelect
|
||||
aria-label="Decision model"
|
||||
options={decisionModels.map((model) => ({ label: model, value: model }))}
|
||||
value={payloadModel(rawPayload) ?? null}
|
||||
onValueChange={handleModelPick}
|
||||
placeholder={decisionModels.length > 0 ? "Pick a decision model" : "No decision models yet"}
|
||||
emptyText="No decision models found"
|
||||
disabled={hasSyntaxError}
|
||||
allowClear={false}
|
||||
/>
|
||||
</div>
|
||||
{decisionModelsLoaded && decisionModels.length === 0 && (
|
||||
<a href={uiHref("models-and-endpoints")} className="text-sm text-primary underline">
|
||||
Add a decision model
|
||||
</a>
|
||||
)}
|
||||
<Select
|
||||
value={apiKeySource}
|
||||
onValueChange={(value) => {
|
||||
|
|
@ -215,7 +214,7 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
|
|||
}}
|
||||
disabled={disabledPersonalKeyCreation}
|
||||
>
|
||||
<SelectTrigger className="w-48" aria-label="Virtual Key Source">
|
||||
<SelectTrigger className="w-44" aria-label="Virtual Key Source">
|
||||
<SelectValue>{apiKeySource === "custom" ? "Virtual Key" : "Current UI Session"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -236,71 +235,32 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<PresetPicker activePreset={activePreset} onPick={handlePresetPick} />
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handlePayloadChange(clearedPayload)}
|
||||
disabled={rawPayload === clearedPayload}
|
||||
>
|
||||
<Eraser />
|
||||
Clear
|
||||
</Button>
|
||||
{view === "json" && (
|
||||
<Button variant="outline" onClick={handleFormatJson} disabled={!rawPayload.trim() || hasSyntaxError}>
|
||||
<Code />
|
||||
Format JSON
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Button variant="outline" onClick={clearRequestState}>
|
||||
Cancel request
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSend} disabled={!validation.isValid || isLoading || !effectiveApiKey}>
|
||||
{isLoading ? <LoaderCircle className="animate-spin" /> : <Send />}
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Alert role="note" aria-label="Decision endpoint notice">
|
||||
<Info />
|
||||
<AlertTitle>{endpoint}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{endpoint === "/v1/decisions" ? (
|
||||
"Uses input and a questions array with predicate, choice, and score questions. Edit the form or JSON and inspect the response JSON in the OpenAI Decisions format."
|
||||
) : (
|
||||
<>
|
||||
Uses state and a questions object with{" "}
|
||||
<SimpleTooltip content={NOUL_HELP} className="underline decoration-dotted underline-offset-2">
|
||||
noul
|
||||
</SimpleTooltip>
|
||||
, choice, and score questions in the System One format.
|
||||
</>
|
||||
)}{" "}
|
||||
Pick a decision model under Model, or omit model to use the proxy's configured default.{" "}
|
||||
<a href={DECISIONS_DOCS_URL} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
How to call /v1/decisions and /v1/systemone
|
||||
</a>
|
||||
{" · "}
|
||||
<a href={DECISION_MODELS_DISCUSSION_URL} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
Give us feedback on what you want for decision models
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-4 xl:min-h-0 xl:flex-1 xl:grid-cols-2">
|
||||
<section className="flex min-h-96 flex-col xl:min-h-0" aria-label="Decisions request editor">
|
||||
<Tabs
|
||||
value={view}
|
||||
onValueChange={(value) => (value === "form" || value === "json") && setView(value)}
|
||||
className="min-h-0 flex-1"
|
||||
>
|
||||
<TabsList aria-label="Request editor view">
|
||||
<TabsTrigger value="form">Form</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<TabsList aria-label="Request editor view">
|
||||
<TabsTrigger value="form">Form</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-1">
|
||||
<PresetPicker activePreset={activePreset} onPick={handlePresetPick} />
|
||||
<SimpleTooltip content="How to call /v1/decisions and /v1/systemone">
|
||||
<a
|
||||
href={DECISIONS_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="How to call /v1/decisions and /v1/systemone"
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon" }), "shrink-0")}
|
||||
>
|
||||
<BookOpen />
|
||||
</a>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="form" className="flex min-h-0 flex-col">
|
||||
{validated.endpoint === "/v1/decisions" ? (
|
||||
<OpenAIDecisionsForm
|
||||
|
|
@ -328,20 +288,53 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation =
|
|||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handlePayloadChange(clearedPayload)}
|
||||
disabled={rawPayload === clearedPayload}
|
||||
>
|
||||
<Eraser />
|
||||
Clear
|
||||
</Button>
|
||||
{view === "json" && (
|
||||
<Button variant="ghost" onClick={handleFormatJson} disabled={!rawPayload.trim() || hasSyntaxError}>
|
||||
<Code />
|
||||
Format JSON
|
||||
</Button>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{isLoading && (
|
||||
<Button variant="outline" onClick={clearRequestState}>
|
||||
Cancel request
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="lg"
|
||||
className="min-w-32"
|
||||
onClick={handleSend}
|
||||
disabled={!validation.isValid || isLoading || !effectiveApiKey}
|
||||
>
|
||||
{isLoading ? <LoaderCircle className="animate-spin" /> : <Send />}
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
|
||||
<ResponseView
|
||||
response={systemOne.data?.response}
|
||||
view={view}
|
||||
fallbackModel={validation.payload?.model}
|
||||
latencyMs={systemOne.data?.latencyMs}
|
||||
error={systemOne.error?.message}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{view === "form" && validated.endpoint === "/v1/systemone" && (
|
||||
<QuestionBreakdown payload={validated.validation.payload} />
|
||||
)}
|
||||
</section>
|
||||
|
||||
{hasResult && (
|
||||
<section className="grid content-start gap-4 xl:min-h-0 xl:overflow-auto" aria-label="Decisions results">
|
||||
<ResponseView
|
||||
response={systemOne.data?.response}
|
||||
view={view}
|
||||
fallbackModel={validation.payload?.model}
|
||||
latencyMs={systemOne.data?.latencyMs}
|
||||
error={systemOne.error?.message}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue