mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-21 19:42:48 +05:30
feat(gui): Duplicate existing model instances (#32)
(fixes Allow Instance Duplication Fixes #32)
This commit is contained in:
@@ -271,6 +271,12 @@ export async function createProviderInstance(
|
||||
);
|
||||
}
|
||||
|
||||
export async function duplicateProviderInstance(
|
||||
id: string,
|
||||
): Promise<ModelProviderInstance | null> {
|
||||
return ProviderManager.duplicate(id);
|
||||
}
|
||||
|
||||
export async function deleteProviderInstance(id: string): Promise<void> {
|
||||
ProviderManager.delete(id);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
setActiveProviderInstance,
|
||||
regenerateEmbeddings,
|
||||
deleteProviderInstance,
|
||||
duplicateProviderInstance,
|
||||
fetchAvailableModels,
|
||||
fetchAvailableModelsForInstance,
|
||||
} from "@/app/actions";
|
||||
@@ -338,6 +339,23 @@ export function ProviderInstancesConfig({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
if (selectedInstanceId === "new" || selectedInstanceId === null) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const duplicated = await duplicateProviderInstance(selectedInstanceId);
|
||||
if (duplicated) {
|
||||
setSelectedInstanceId(duplicated.id);
|
||||
await onChanged();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="mb-8 flex min-h-[600px] flex-col">
|
||||
{error && (
|
||||
@@ -646,16 +664,26 @@ export function ProviderInstancesConfig({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex flex-row gap-2">
|
||||
{selectedInstanceId !== "new" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleDuplicate}
|
||||
disabled={loading}
|
||||
>
|
||||
Duplicate
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
|
||||
@@ -25,15 +25,8 @@ import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Bot, UserRound } from "lucide-react";
|
||||
|
||||
export function HomeView() {
|
||||
const router = useRouter();
|
||||
@@ -192,7 +185,7 @@ export function HomeView() {
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto w-full relative">
|
||||
<div className="relative z-10 mx-auto max-w-[1024px] px-10 py-12">
|
||||
<div className="relative z-10 mx-auto max-w-5xl px-10 py-12">
|
||||
<div className="animate-fade-in">
|
||||
{/* Centered Big Logo */}
|
||||
<div className="flex flex-col items-center justify-center mb-10 pt-4">
|
||||
@@ -237,7 +230,7 @@ export function HomeView() {
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 w-72 border border-border/30 bg-card p-5 shadow-sm transition-all flex flex-col justify-between h-[148px]"
|
||||
className="shrink-0 w-72 border border-border/30 bg-card p-5 shadow-sm transition-all flex flex-col justify-between h-37"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-2/3" />
|
||||
@@ -264,7 +257,7 @@ export function HomeView() {
|
||||
? undefined
|
||||
: () => handleResume(s.id)
|
||||
}
|
||||
className={`flex-shrink-0 w-72 border border-border/30 bg-card p-5 shadow-sm transition-all relative group ${
|
||||
className={`shrink-0 w-72 border border-border/30 bg-card p-5 shadow-sm transition-all relative group ${
|
||||
providerInstances.length === 0
|
||||
? "opacity-50 cursor-not-allowed filter grayscale"
|
||||
: "cursor-pointer hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm"
|
||||
@@ -310,8 +303,8 @@ export function HomeView() {
|
||||
</h2>
|
||||
{loadingData ? (
|
||||
<div className="flex overflow-x-auto gap-6 pb-4 scrollbar-thin scrollbar-thumb-border/20">
|
||||
<Link href="/builder" className="no-underline flex-shrink-0">
|
||||
<div className="w-64 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all flex flex-col justify-between h-full min-h-[148px]">
|
||||
<Link href="/builder" className="no-underline shrink-0">
|
||||
<div className="w-64 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all flex flex-col justify-between h-full min-h-37">
|
||||
<div>
|
||||
<strong className="text-body-md text-foreground block mb-1">
|
||||
Build a scenario
|
||||
@@ -331,7 +324,7 @@ export function HomeView() {
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 w-64 border border-border/30 bg-card p-5 shadow-sm flex flex-col justify-between h-[148px]"
|
||||
className="shrink-0 w-64 border border-border/30 bg-card p-5 shadow-sm flex flex-col justify-between h-37"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
@@ -345,8 +338,8 @@ export function HomeView() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex overflow-x-auto gap-6 pb-4 scrollbar-thin scrollbar-thumb-border/20">
|
||||
<Link href="/builder" className="no-underline flex-shrink-0">
|
||||
<div className="w-64 border border-primary bg-primary p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all flex flex-col justify-between h-full min-h-[148px]">
|
||||
<Link href="/builder" className="no-underline shrink-0">
|
||||
<div className="w-64 border border-primary bg-primary p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all flex flex-col justify-between h-full min-h-37">
|
||||
<div>
|
||||
<strong className="text-body-md text-surface block mb-1">
|
||||
Build a scenario
|
||||
@@ -382,7 +375,7 @@ export function HomeView() {
|
||||
open={!!scenarioForModal}
|
||||
onOpenChange={(open) => !open && setScenarioForModal(null)}
|
||||
>
|
||||
<DialogContent className="max-w-[400px]">
|
||||
<DialogContent className="max-w-100">
|
||||
<DialogHeader className="border-b border-dotted border-border/20 pb-4 mb-2">
|
||||
<DialogTitle>Start Scenario</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -412,28 +405,37 @@ export function HomeView() {
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
|
||||
Simulation Mode / Play as
|
||||
</label>
|
||||
<Select
|
||||
value={selectedEntityForModal}
|
||||
onValueChange={(val) =>
|
||||
setSelectedEntityForModal(val || "")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="-- Run Fully Autonomously --" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="">
|
||||
-- Run Fully Autonomously --
|
||||
</SelectItem>
|
||||
{modalEntities.map((ent) => (
|
||||
<SelectItem key={ent.id} value={ent.id}>
|
||||
Play as {ent.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={
|
||||
selectedEntityForModal === ""
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => setSelectedEntityForModal("")}
|
||||
className="flex-1 min-w-40"
|
||||
>
|
||||
<Bot />
|
||||
Run Fully Autonomously
|
||||
</Button>
|
||||
{modalEntities.map((ent) => (
|
||||
<Button
|
||||
key={ent.id}
|
||||
type="button"
|
||||
variant={
|
||||
selectedEntityForModal === ent.id
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => setSelectedEntityForModal(ent.id)}
|
||||
className="flex-1 min-w-40"
|
||||
>
|
||||
<UserRound />
|
||||
Play as {ent.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-[800px] sm:max-w-[800px] h-[85vh] overflow-hidden flex flex-col p-0 gap-0 border-2">
|
||||
<DialogContent className="max-w-200 sm:max-w-200 h-[85vh] overflow-hidden flex flex-col p-0 gap-0 border-2">
|
||||
<DialogHeader className="px-6 pt-5 pb-4 border-b">
|
||||
<DialogTitle className="text-lg font-head tracking-wide text-primary flex items-center justify-between">
|
||||
<span>Memory Handoff Details — {entry.entityName}</span>
|
||||
@@ -240,7 +240,7 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
|
||||
Raw JSON Output
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border flex-1 overflow-y-auto max-h-[500px]">
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border flex-1 overflow-y-auto max-h-125">
|
||||
{handoffResult
|
||||
? JSON.stringify(handoffResult, null, 2)
|
||||
: "No JSON Output recorded."}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { hydrate } from "@omnia/voice";
|
||||
import { Brain, PersonStanding, Speech } from "lucide-react";
|
||||
import {
|
||||
Alert,
|
||||
AlertAction,
|
||||
@@ -26,19 +27,14 @@ function IntentTag({
|
||||
playerId: string;
|
||||
entities: SimSnapshot["entities"];
|
||||
}) {
|
||||
const labels: Record<string, string> = {
|
||||
monologue: "thought",
|
||||
thought: "thought",
|
||||
dialogue: "dialogue",
|
||||
action: "action",
|
||||
const icons: Record<string, React.ReactNode> = {
|
||||
monologue: <Brain className="size-4" />,
|
||||
thought: <Brain className="size-4" />,
|
||||
dialogue: <Speech className="size-4" />,
|
||||
action: <PersonStanding className="size-4" />,
|
||||
};
|
||||
|
||||
const label = labels[intent.type] || intent.type;
|
||||
|
||||
let outcome = "";
|
||||
if (intent.type === "action") {
|
||||
outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`;
|
||||
}
|
||||
const icon = icons[intent.type] || null;
|
||||
|
||||
const viewerAliasesMap = new Map<string, string>();
|
||||
if (entities) {
|
||||
@@ -69,12 +65,28 @@ function IntentTag({
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
const invalidActionReason =
|
||||
intent.type === "action" && !intent.isValid && intent.reason
|
||||
? ` (${intent.reason})`
|
||||
: "";
|
||||
|
||||
const invalidActionClassName =
|
||||
intent.type === "action" && !intent.isValid ? " text-destructive" : "";
|
||||
|
||||
return (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
[{label}] “{textToDisplay}”{modifiersStr}
|
||||
{outcome}
|
||||
{intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""}
|
||||
</span>
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground inline-flex items-start gap-1">
|
||||
<span className="mt-0.5 inline-flex shrink-0 items-center justify-center">
|
||||
{icon}
|
||||
</span>
|
||||
<span className={invalidActionClassName}>
|
||||
“{textToDisplay}”{modifiersStr}
|
||||
{invalidActionReason}
|
||||
{intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""}
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,45 +117,47 @@ function LogEntryCard({
|
||||
entry: SimSnapshot["log"][number];
|
||||
onShowPrompt: (entry: SimSnapshot["log"][number]) => void;
|
||||
isPlayerCard: boolean;
|
||||
playerAliases: Record<string, string>;
|
||||
playerAlciases: Record<string, string>;
|
||||
playerId: string;
|
||||
entities: SimSnapshot["entities"];
|
||||
}) {
|
||||
const showMenu = !!(entry.rawPrompt || entry.decoderPrompt);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border p-4 shadow-[2px_2px_0_0_var(--border)]",
|
||||
isPlayerCard
|
||||
? "border-primary bg-surface-container-low"
|
||||
: "border-border/30 bg-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2 border-b border-dotted border-border/20 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<strong className="text-body-md font-bold text-foreground">
|
||||
{entry.entityName}
|
||||
</strong>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
Turn {entry.turn} · {formatSimTime(entry.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{showMenu && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onShowPrompt(entry)}
|
||||
title="View Raw Prompts & Token Usage"
|
||||
>
|
||||
☰
|
||||
</Button>
|
||||
<div className={cn("mb-2")}>
|
||||
<div
|
||||
className={cn(
|
||||
"border p-4 shadow-sm",
|
||||
isPlayerCard
|
||||
? "border-primary bg-surface-container-low"
|
||||
: "border-border/30 bg-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2 border-b border-dotted border-border/20 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<strong className="text-body-md font-bold text-foreground">
|
||||
{entry.entityName}
|
||||
</strong>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
Turn {entry.turn} · {formatSimTime(entry.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{showMenu && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onShowPrompt(entry)}
|
||||
title="View Raw Prompts & Token Usage"
|
||||
>
|
||||
☰
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-body-md leading-relaxed mb-3 text-foreground/90 whitespace-pre-wrap">
|
||||
{entry.narrativeProse}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-body-md leading-relaxed mb-3 text-foreground/90 whitespace-pre-wrap">
|
||||
{entry.narrativeProse}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 mt-2 border-t border-dotted border-border/10 pt-2">
|
||||
<div className={cn("mt-3 ms-3")}>
|
||||
{entry.intents.map((intent, i) => (
|
||||
<IntentTag
|
||||
key={i}
|
||||
@@ -189,7 +203,7 @@ export function InteractView({
|
||||
<>
|
||||
{/* Scrollable Center Viewport */}
|
||||
<main className="flex-1 overflow-y-auto px-8 py-6">
|
||||
<div className="flex flex-col gap-4 max-w-[800px] mx-auto pb-12">
|
||||
<div className="flex flex-col gap-4 max-w-200 mx-auto pb-12">
|
||||
{snapshot.log.map((entry, i) => {
|
||||
if (entry.isHandoff) {
|
||||
return (
|
||||
@@ -245,14 +259,14 @@ export function InteractView({
|
||||
|
||||
{/* Sticky Chat / Interaction Input Footer */}
|
||||
<footer className="sticky bottom-0 bg-background/95 backdrop-blur-xs border-t border-dotted border-border/20 px-8 py-4 z-10 shrink-0">
|
||||
<div className="max-w-[800px] mx-auto">
|
||||
<div className="max-w-200 mx-auto">
|
||||
{snapshot.status === "waiting_player" && snapshot.waitingEntity ? (
|
||||
<div className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)]">
|
||||
<div className="border border-border/30 bg-card p-4 shadow-sm">
|
||||
<details className="mb-3">
|
||||
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
|
||||
<strong>Your context as {snapshot.waitingEntity.name}</strong>
|
||||
</summary>
|
||||
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-[150px] overflow-y-auto mt-2 font-mono">
|
||||
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-37.5 overflow-y-auto mt-2 font-mono">
|
||||
{snapshot.waitingEntity.userContext}
|
||||
</pre>
|
||||
</details>
|
||||
@@ -271,7 +285,7 @@ export function InteractView({
|
||||
</form>
|
||||
</div>
|
||||
) : snapshot.status === "done" || snapshot.status === "error" ? (
|
||||
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-[2px_2px_0_0_var(--border)]">
|
||||
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-sm">
|
||||
<span className="text-sm font-mono text-muted-foreground">
|
||||
{snapshot.status === "error"
|
||||
? "Simulation finished with an error."
|
||||
|
||||
@@ -52,21 +52,26 @@ export function PromptAnalyzer({
|
||||
maxContext > 0 ? (inputTokens / maxContext) * 100 : 0;
|
||||
const isAbsolute = maxContext > 0 && usagePctOfContext >= 20;
|
||||
|
||||
const getColorClass = (type: string) => {
|
||||
switch (type) {
|
||||
case "system":
|
||||
return "bg-blue-500";
|
||||
case "world":
|
||||
return "bg-emerald-500";
|
||||
case "events":
|
||||
return "bg-purple-500";
|
||||
case "memories":
|
||||
return "bg-pink-500";
|
||||
case "input":
|
||||
return "bg-amber-500";
|
||||
default:
|
||||
return "bg-slate-500";
|
||||
}
|
||||
const legentColors = [
|
||||
"bg-blue-500",
|
||||
"bg-emerald-500",
|
||||
"bg-purple-500",
|
||||
"bg-orange-500",
|
||||
"bg-pink-500",
|
||||
"bg-amber-500",
|
||||
"bg-teal-500",
|
||||
"bg-cyan-500",
|
||||
"bg-indigo-500",
|
||||
"bg-violet-500",
|
||||
"bg-rose-500",
|
||||
"bg-sky-500",
|
||||
"bg-lime-500",
|
||||
"bg-fuchsia-500",
|
||||
"bg-red-500",
|
||||
];
|
||||
|
||||
const getColorClass = (index: number) => {
|
||||
return legentColors[index % legentColors.length];
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -106,7 +111,7 @@ export function PromptAnalyzer({
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`h-full transition-all duration-300 ${getColorClass(item.type)}`}
|
||||
className={`h-full transition-all duration-300 ${getColorClass(idx)}`}
|
||||
style={{ width: `${widthPct}%` }}
|
||||
title={`${item.label}: ${item.tokens} tokens (${item.pct.toFixed(1)}%)`}
|
||||
/>
|
||||
@@ -129,7 +134,7 @@ export function PromptAnalyzer({
|
||||
<AccordionTrigger className="text-sm py-2.5 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-2.5 h-2.5 rounded-sm ${getColorClass(item.type)}`}
|
||||
className={`inline-block w-2.5 h-2.5 rounded-sm ${getColorClass(idx)}`}
|
||||
/>
|
||||
<span>{item.label}:</span>
|
||||
<span className="text-muted-foreground font-normal">
|
||||
@@ -139,7 +144,7 @@ export function PromptAnalyzer({
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre className="m-0 p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[300px] overflow-y-auto">
|
||||
<pre className="m-0 p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-75 overflow-y-auto">
|
||||
{item.content}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
@@ -161,7 +166,7 @@ export function PromptAnalyzer({
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded border-2">
|
||||
<pre className="m-0 p-3 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground max-h-[250px] overflow-y-auto">
|
||||
<pre className="m-0 p-3 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground max-h-62.5 overflow-y-auto">
|
||||
{outputText}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,7 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
}
|
||||
modelName={validatorCall.usage?.modelName}
|
||||
providerInstanceName={validatorCall.usage?.providerInstanceName}
|
||||
outputLabel={`LLM Output (Validation for: "${validatorCall.intentContent}")`}
|
||||
outputLabel={`LLM Output`}
|
||||
outputText={JSON.stringify(validatorCall.response, null, 2)}
|
||||
outputTokens={validatorCall.usage?.outputTokens}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ScenarioCard({
|
||||
return (
|
||||
<div
|
||||
onClick={disabled ? undefined : onClick}
|
||||
className={`flex-shrink-0 w-64 border border-border/30 bg-card p-5 shadow-sm transition-all ${
|
||||
className={`shrink-0 w-64 border border-border/30 bg-card p-5 shadow-sm transition-all ${
|
||||
disabled
|
||||
? "opacity-50 cursor-not-allowed filter grayscale"
|
||||
: "cursor-pointer hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm"
|
||||
|
||||
@@ -62,6 +62,50 @@ export class ProviderManager {
|
||||
};
|
||||
}
|
||||
|
||||
static duplicate(id: string): ModelProviderInstance | null {
|
||||
const db = getDb();
|
||||
const source = db
|
||||
.prepare("SELECT * FROM provider_instances WHERE id = ?")
|
||||
.get(id) as DbRow | undefined;
|
||||
if (!source) return null;
|
||||
|
||||
const newId = "provider-" + Date.now();
|
||||
const newName = `${source.name} (Copy)`;
|
||||
|
||||
const activeCount = db
|
||||
.prepare(
|
||||
"SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1 AND type = ?",
|
||||
)
|
||||
.get(source.type) as { count: number };
|
||||
const isActive = activeCount.count === 0 ? 1 : 0;
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext, endpointUrl)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
).run(
|
||||
newId,
|
||||
newName,
|
||||
source.providerName,
|
||||
source.apiKey,
|
||||
isActive,
|
||||
source.modelName,
|
||||
source.type,
|
||||
source.maxContext,
|
||||
source.endpointUrl,
|
||||
);
|
||||
|
||||
return {
|
||||
id: newId,
|
||||
name: newName,
|
||||
providerName: source.providerName,
|
||||
apiKey: source.apiKey,
|
||||
isActive: isActive === 1,
|
||||
modelName: source.modelName || undefined,
|
||||
type: source.type as "generative" | "embedding",
|
||||
maxContext: source.maxContext,
|
||||
endpointUrl: source.endpointUrl || undefined,
|
||||
};
|
||||
}
|
||||
static delete(id: string): void {
|
||||
const db = getDb();
|
||||
const provider = db
|
||||
|
||||
Reference in New Issue
Block a user