mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(playground): add PresetPicker + ImprovePromptButton
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
// src/app/(dashboard)/dashboard/playground/components/ImprovePromptButton.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
import { useImprovePrompt } from "../hooks/useImprovePrompt";
|
||||
import type { ConfigState } from "./StudioConfigPane";
|
||||
|
||||
interface ImprovePromptButtonProps {
|
||||
configState: ConfigState;
|
||||
setConfigState: (s: ConfigState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ImprovePromptButton — "✨ Improve prompt" button with quota-consumption warning.
|
||||
*
|
||||
* Flow: click → confirmation modal → confirm → calls useImprovePrompt.improve()
|
||||
* → on success, updates configState.systemPrompt and/or configState.params.
|
||||
*
|
||||
* D8: uses the model configured in the Config pane (never overrides with cheap model).
|
||||
*/
|
||||
export default function ImprovePromptButton({ configState, setConfigState }: ImprovePromptButtonProps) {
|
||||
const { loading, error, improve } = useImprovePrompt();
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [improveError, setImproveError] = useState<string | null>(null);
|
||||
|
||||
async function handleConfirm() {
|
||||
setConfirmOpen(false);
|
||||
setImproveError(null);
|
||||
|
||||
const model = configState.model.trim();
|
||||
if (!model) {
|
||||
setImproveError("Please set a model in the Config pane first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await improve({
|
||||
system: configState.systemPrompt || undefined,
|
||||
model,
|
||||
});
|
||||
|
||||
if (result == null) {
|
||||
setImproveError(error ?? "Improve prompt failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply improved versions if returned
|
||||
const next = { ...configState };
|
||||
|
||||
if (result.improvedSystem != null) {
|
||||
next.systemPrompt = result.improvedSystem;
|
||||
}
|
||||
|
||||
setConfigState(next);
|
||||
}
|
||||
|
||||
const isDisabled = loading || !configState.model.trim();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setImproveError(null);
|
||||
setConfirmOpen(true);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded border border-border text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-40 disabled:cursor-not-allowed self-start"
|
||||
aria-label="Improve prompt using AI"
|
||||
title={!configState.model.trim() ? "Set a model first" : "Improve your prompt with AI"}
|
||||
>
|
||||
<span className="text-[13px]">✨</span>
|
||||
{loading ? "Improving…" : "Improve prompt"}
|
||||
</button>
|
||||
|
||||
{improveError && (
|
||||
<p className="text-[11px] text-destructive">{improveError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quota confirmation modal */}
|
||||
{confirmOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onClick={() => setConfirmOpen(false)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Confirm improve prompt"
|
||||
>
|
||||
<div
|
||||
className="bg-surface border border-border rounded-xl p-5 w-80 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<span className="text-[24px] shrink-0">✨</span>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-main mb-1">
|
||||
Improve prompt
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted">
|
||||
This will send your current system prompt to{" "}
|
||||
<code className="font-mono text-primary">{configState.model}</code>
|
||||
{" "}to generate an improved version.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted mt-1.5 font-medium">
|
||||
This action will consume model quota.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setConfirmOpen(false)}
|
||||
className="text-xs px-3 py-1.5 rounded border border-border text-text-muted hover:text-text-main transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleConfirm()}
|
||||
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded bg-primary text-white hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<span className="text-[12px]">✨</span>
|
||||
Improve
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
// src/app/(dashboard)/dashboard/playground/components/PresetPicker.tsx
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePresets } from "../hooks/usePresets";
|
||||
import type { ConfigState } from "./StudioConfigPane";
|
||||
|
||||
interface PresetPickerProps {
|
||||
configState: ConfigState;
|
||||
setConfigState: (s: ConfigState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* PresetPicker — load/save playground presets.
|
||||
*
|
||||
* Presets are stored in DB via /api/playground/presets (F3 backend).
|
||||
* Load applies preset values to configState; Save opens a name-input modal.
|
||||
*/
|
||||
export default function PresetPicker({ configState, setConfigState }: PresetPickerProps) {
|
||||
const { presets, loading, list, create, remove } = usePresets();
|
||||
const [saveModalOpen, setSaveModalOpen] = useState(false);
|
||||
const [presetName, setPresetName] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
// Load presets on mount
|
||||
useEffect(() => {
|
||||
void list();
|
||||
}, [list]);
|
||||
|
||||
function handleLoad(presetId: string) {
|
||||
const preset = presets.find((p) => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
setConfigState({
|
||||
...configState,
|
||||
endpoint: preset.endpoint as ConfigState["endpoint"],
|
||||
model: preset.model,
|
||||
systemPrompt: preset.system ?? configState.systemPrompt,
|
||||
params: {
|
||||
...configState.params,
|
||||
...(typeof preset.params === "object" && preset.params != null ? preset.params : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!presetName.trim()) {
|
||||
setSaveError("Name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
|
||||
const result = await create({
|
||||
name: presetName.trim(),
|
||||
endpoint: configState.endpoint,
|
||||
model: configState.model,
|
||||
system: configState.systemPrompt || null,
|
||||
params: configState.params as Record<string, unknown>,
|
||||
});
|
||||
|
||||
setSaving(false);
|
||||
|
||||
if (result == null) {
|
||||
setSaveError("Failed to save preset");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveModalOpen(false);
|
||||
setPresetName("");
|
||||
}
|
||||
|
||||
function openSave() {
|
||||
setSaveModalOpen(true);
|
||||
setPresetName("");
|
||||
setSaveError(null);
|
||||
}
|
||||
|
||||
function closeSave() {
|
||||
setSaveModalOpen(false);
|
||||
setPresetName("");
|
||||
setSaveError(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||
Presets
|
||||
</span>
|
||||
|
||||
{/* Load preset select */}
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
disabled={loading || presets.length === 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
handleLoad(e.target.value);
|
||||
// Reset select to placeholder after loading
|
||||
e.target.value = "";
|
||||
}
|
||||
}}
|
||||
defaultValue=""
|
||||
className="flex-1 text-xs bg-surface border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary text-text-main disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label="Load a preset"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{loading ? "Loading…" : presets.length === 0 ? "No presets" : "Load preset…"}
|
||||
</option>
|
||||
{presets.map((preset) => (
|
||||
<option key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={openSave}
|
||||
className="text-xs px-2.5 py-1.5 rounded border border-border text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5 transition-colors shrink-0"
|
||||
aria-label="Save current config as preset"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Preset list with delete buttons (compact) */}
|
||||
{presets.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{presets.map((preset) => (
|
||||
<div
|
||||
key={preset.id}
|
||||
className="flex items-center justify-between text-[11px] text-text-muted hover:text-text-main group"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleLoad(preset.id)}
|
||||
className="truncate text-left flex-1 hover:text-primary transition-colors"
|
||||
aria-label={`Load preset "${preset.name}"`}
|
||||
>
|
||||
{preset.name}
|
||||
<span className="ml-1.5 opacity-60">{preset.model}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void remove(preset.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 text-text-muted hover:text-destructive transition-all"
|
||||
aria-label={`Delete preset "${preset.name}"`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save preset modal */}
|
||||
{saveModalOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onClick={closeSave}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Save preset"
|
||||
>
|
||||
<div
|
||||
className="bg-surface border border-border rounded-xl p-5 w-80 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-text-main mb-4">Save preset</h3>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={presetName}
|
||||
onChange={(e) => setPresetName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleSave();
|
||||
if (e.key === "Escape") closeSave();
|
||||
}}
|
||||
placeholder="Preset name"
|
||||
autoFocus
|
||||
className="text-xs bg-bg-alt border border-border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary text-text-main"
|
||||
/>
|
||||
|
||||
{saveError && (
|
||||
<p className="text-xs text-destructive">{saveError}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={closeSave}
|
||||
className="text-xs px-3 py-1.5 rounded border border-border text-text-muted hover:text-text-main transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleSave()}
|
||||
disabled={saving}
|
||||
className="text-xs px-3 py-1.5 rounded bg-primary text-white hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user