Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion app/(main)/configurations/prompt-editor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
import { SavedConfig, ConfigVersionItems } from "@/app/lib/types/configs";
import { configState } from "@/app/lib/store/config";
import { DEFAULT_CONFIG } from "@/app/lib/constants";
import { apiFetch } from "@/app/lib/apiClient";

function PromptEditorContent() {

Check warning on line 28 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Function 'PromptEditorContent' has a complexity of 12. Maximum allowed is 10

Check warning on line 28 in app/(main)/configurations/prompt-editor/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Function 'PromptEditorContent' has too many statements (41). Maximum allowed is 20
const searchParams = useSearchParams();
const { sidebarCollapsed } = useApp();
const { activeKey } = useAuth();
Expand Down Expand Up @@ -126,6 +127,29 @@
[],
);

const refreshVersionsForConfig = React.useCallback(
async (configId: string) => {
if (!configId) return;
try {
const apiKey = activeKey?.key ?? "";
const res = await apiFetch<{
success: boolean;
data: ConfigVersionItems[];
}>(`/api/configs/${configId}/versions`, apiKey);
if (res.success && res.data) {
configState.versionItemsCache[configId] = res.data;
setStableVersionItemsMap((prev) => ({
...prev,
[configId]: res.data,
}));
}
} catch (err) {
console.error("Failed to refresh version list:", err);
}
},
[activeKey],
);

const resetEditor = React.useCallback(() => {
setCurrentContent("");
setCurrentConfigBlob(DEFAULT_CONFIG);
Expand Down Expand Up @@ -247,7 +271,11 @@
if (ok) {
setHasUnsavedChanges(false);
setCommitMessage("");
if (wasNewConfig) resetEditor();
if (wasNewConfig) {
resetEditor();
} else if (currentConfigParentId) {
await refreshVersionsForConfig(currentConfigParentId);
}
}
};

Expand Down
71 changes: 71 additions & 0 deletions app/(main)/evaluations/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useRouter, useParams } from "next/navigation";
import { apiFetch } from "@/app/lib/apiClient";
import { invalidateConfigCache } from "@/app/lib/utils";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import type {
Expand Down Expand Up @@ -50,7 +51,7 @@
DownloadIcon,
} from "@/app/components/icons";

export default function EvaluationReport() {

Check warning on line 54 in app/(main)/evaluations/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build

Function 'EvaluationReport' has too many statements (36). Maximum allowed is 20
const router = useRouter();
const params = useParams();
const toast = useToast();
Expand All @@ -70,6 +71,7 @@
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [exportFormat, setExportFormat] = useState<"row" | "grouped">("row");
const [isResyncing, setIsResyncing] = useState(false);
const [isImprovingPrompt, setIsImprovingPrompt] = useState(false);
const [showNoTracesModal, setShowNoTracesModal] = useState(false);

const fetchJobDetails = useCallback(async () => {
Expand Down Expand Up @@ -180,6 +182,51 @@
}
};

const handleIteratePrompt = async () => {
if (!isAuthenticated || !jobId) return;
setIsImprovingPrompt(true);
try {
const data = await apiFetch<{
success: boolean;
error?: string;
data?: {
config_id?: string;
id?: string;
version?: number;
} | null;
}>(`/api/evaluations/${jobId}/improve-prompt`, apiKey, {
method: "POST",
body: JSON.stringify({}),
});

if (!data.success) {
toast.error(data.error || "Failed to iterate on prompt");
return;
}

invalidateConfigCache();
toast.success("Prompt iteration created");

const newConfigId = data.data?.config_id ?? data.data?.id;
const newVersion = data.data?.version;
if (newConfigId) {
const versionParam =
typeof newVersion === "number" ? `&version=${newVersion}` : "";
router.push(
`/configurations/prompt-editor?config=${encodeURIComponent(
newConfigId,
)}${versionParam}&from=evaluations`,
);
}
Comment thread
Ayush8923 marked this conversation as resolved.
} catch (err: unknown) {
toast.error(
`Failed to iterate on prompt: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsImprovingPrompt(false);
}
};

const handleResync = async () => {
if (!isAuthenticated || !jobId) return;

Expand Down Expand Up @@ -344,6 +391,30 @@
<span className="hidden sm:inline">View Config</span>
<span className="sm:hidden">Config</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={handleIteratePrompt}
disabled={
job.status.toLowerCase() !== "completed" ||
isImprovingPrompt ||
isFormatSwitching ||
isResyncing
}
title={
job.status.toLowerCase() !== "completed"
? "Only available for completed evaluations"
: undefined
}
>
{isImprovingPrompt && <Loader size="sm" />}
<span className="hidden sm:inline">
{isImprovingPrompt ? "Iterating..." : "Iterate on Prompt"}
</span>
<span className="sm:hidden">
{isImprovingPrompt ? "..." : "Iterate"}
</span>
</Button>
<Button
variant="primary"
size="sm"
Expand Down
39 changes: 39 additions & 0 deletions app/api/evaluations/[id]/improve-prompt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;

let body: unknown = undefined;
try {
body = await request.json();
} catch {
body = {};
}

const { status, data } = await apiClient(
request,
`/api/v1/evaluations/${id}/improve-prompt`,
{
method: "POST",
body: JSON.stringify(body ?? {}),
},
);

return NextResponse.json(data ?? { error: "Backend error" }, { status });
} catch (error: unknown) {
console.error("improve-prompt proxy error:", error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error ? error.message : "Failed to improve prompt",
},
{ status: 500 },
);
}
}
65 changes: 65 additions & 0 deletions app/components/CommitMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { CommitMessageProps, ParsedCommit } from "@/app/lib/types/commit";

const AI_GENERATED_PATTERN =
/^\[AI Generated\]\s*(?:improved from config version\s+v(\d+))?\s*(?:\(Evaluation:\s*([^)]+)\))?\s*(.*)$/i;

export function parseCommitMessage(
message: string | null | undefined,
): ParsedCommit {
if (!message) return { isAIGenerated: false, body: "" };
const match = message.match(AI_GENERATED_PATTERN);
if (!match) return { isAIGenerated: false, body: message };
const [, fromVersionRaw, evalName, body] = match;
return {
isAIGenerated: true,
fromVersion: fromVersionRaw ? parseInt(fromVersionRaw, 10) : undefined,
evaluation: evalName?.trim(),
body: body?.trim() ?? "",
};
}
Comment thread
Ayush8923 marked this conversation as resolved.

export default function CommitMessage({
message,
className = "",
compact = false,
}: CommitMessageProps) {
const parsed = parseCommitMessage(message);

if (!parsed.isAIGenerated) {
return message ? <span className={className}>{message}</span> : null;
}

const metaParts: string[] = [];
if (parsed.fromVersion !== undefined) {
metaParts.push(`From v${parsed.fromVersion}`);
}
if (parsed.evaluation) {
metaParts.push(`Eval: ${parsed.evaluation}`);
}

return (
<div className={`space-y-1 ${className}`}>
<div className="flex flex-wrap items-center gap-1.5">
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold uppercase tracking-wide bg-accent-primary/10 text-accent-primary">
AI Generated
</span>
{metaParts.length > 0 && (
<span className="text-xs text-text-secondary">
{metaParts.join(" • ")}
</span>
)}
</div>
{parsed.body && (
<div
className={
compact
? "text-xs text-text-primary max-h-24 overflow-y-auto pr-1 whitespace-pre-wrap"
: "text-xs text-text-primary whitespace-pre-wrap"
}
>
{parsed.body}
</div>
)}
</div>
);
}
8 changes: 6 additions & 2 deletions app/components/evaluations/DetailedResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { formatScoreValue, getScoreByName } from "@/app/lib/utils";
import { InfoTooltip } from "@/app/components/ui";
import { GroupedResultsTable } from "@/app/components/evaluations";
import { MarkdownContent } from "@/app/components/chat";

interface DetailedResultsTableProps {
job: EvalJob;
Expand Down Expand Up @@ -175,7 +176,10 @@ export default function DetailedResultsTable({

<td className="px-4 py-3 align-top bg-bg-primary">
<div className="text-sm overflow-auto text-text-primary leading-normal max-h-[150px] wrap-break-word">
{answer}
<MarkdownContent
text={answer}
className={"text-sm leading-relaxed"}
/>
</div>
</td>

Expand All @@ -190,7 +194,7 @@ export default function DetailedResultsTable({
>
<div className="flex items-center justify-center gap-2">
<div
className={`inline-block px-2 py-1 rounded text-xs font-medium border-border ${bg === "transparent" ? "border" : ""}`}
className={`inline-block px-2 py-1 rounded-full text-xs font-medium border-border ${bg === "transparent" ? "border" : ""}`}
style={{
color,
backgroundColor: bg,
Expand Down
23 changes: 4 additions & 19 deletions app/components/icons/prompt-editor/SpinnerIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,10 @@ interface IconProps {

export default function SpinnerIcon({ className, style }: IconProps) {
return (
<svg
className={`w-3 h-3 animate-spin ${className ?? ""}`}
viewBox="0 0 24 24"
fill="none"
<span
aria-hidden
style={style}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
className={`inline-block w-3.5 h-3.5 rounded-full animate-spin border-2 border-accent-primary/20 border-t-accent-primary [animation-duration:0.7s] ${className ?? ""}`}
/>
);
}
1 change: 1 addition & 0 deletions app/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as VersionPill } from "./configurations/VersionPill";
export { default as DatasetListSkeleton } from "./DatasetListSkeleton";
export { default as RunsListSkeleton } from "./RunsListSkeleton";
export { default as ResultsTableSkeleton } from "./ResultsTableSkeleton";
export { default as CommitMessage } from "./CommitMessage";
1 change: 0 additions & 1 deletion app/components/prompt-editor/ConfigEditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export default function ConfigEditorPane({
.catch(() => {});
}, [apiKey]);

// Close the save modal when a save just completed successfully.
useEffect(() => {
if (wasSavingRef.current && !isSaving && commitMessage === "") {
setShowSaveModal(false);
Expand Down
7 changes: 3 additions & 4 deletions app/components/prompt-editor/HistorySidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { Button, Loader } from "@/app/components/ui";
import { VersionPill } from "@/app/components";
import { CommitMessage, VersionPill } from "@/app/components";
import { ArrowLeftIcon } from "@/app/components/icons";
import {
ConfigPublic,
Expand Down Expand Up @@ -53,8 +53,8 @@ function VersionRow({
</div>

{item.commit_message && (
<div className="text-xs mb-1 text-text-primary">
{item.commit_message}
<div className="mb-2">
<CommitMessage message={item.commit_message} compact />
</div>
)}

Expand Down Expand Up @@ -93,7 +93,6 @@ function VersionRow({
);
}

// Single-config version history (when a config is loaded in the editor)
interface SingleConfigHistoryProps {
configId: string;
configName: string;
Expand Down
12 changes: 12 additions & 0 deletions app/lib/types/commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface ParsedCommit {
isAIGenerated: boolean;
fromVersion?: number;
evaluation?: string;
body: string;
}

export interface CommitMessageProps {
message: string | null | undefined;
className?: string;
compact?: boolean;
}
Loading