inference-playground / src /lib /components /flow /actions /PersonasModal.svelte
enzostvs's picture
enzostvs HF Staff
ui for persona
dc766b5
<script lang="ts">
import { ArrowLeft, Drama, Info, Pencil, Plus, Trash2 } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Sheet from '$lib/components/ui/sheet/index.js';
import { breakpointsState } from '$lib/state/breakpoints.svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
import {
personasState,
addPersona,
deletePersona,
savePersona,
setSelectedPersona,
DEFAULT_PERSONA
} from '$lib/state/personas.svelte';
import { Input } from '$lib/components/ui/input';
import type { Persona } from '$lib/state/personas.svelte';
import { getAvatarClass, getInitials } from '$lib';
// bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300
// bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300
// bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300
// bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300
// bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300
// bg-slate-100 text-slate-600 dark:bg-slate-700/50 dark:text-slate-300
let open = $state(false);
let openFormPersona = $state.raw<string | null>(null);
let formName = $state('');
let formDescription = $state('');
let formInstructions = $state('');
function openNew() {
formName = '';
formDescription = '';
formInstructions = '';
openFormPersona = 'new';
}
function openEdit(persona: Persona) {
if (persona.id === DEFAULT_PERSONA.id) return;
formName = persona.name;
formDescription = persona.description ?? '';
formInstructions = persona.instructions;
openFormPersona = persona.id;
}
function handleSave() {
if (!formName.trim()) return;
if (openFormPersona === 'new') {
addPersona({
name: formName.trim(),
description: formDescription.trim(),
instructions: formInstructions.trim()
});
} else if (openFormPersona) {
savePersona(openFormPersona, {
id: openFormPersona,
name: formName.trim(),
description: formDescription.trim(),
instructions: formInstructions.trim()
});
}
openFormPersona = null;
}
</script>
<Sheet.Root bind:open>
<Sheet.Trigger class="">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button variant="outline" size={breakpointsState.isMobile ? 'icon-sm' : 'icon-lg'}>
<Drama class={breakpointsState.isMobile ? 'size-4' : 'size-5'} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content>Use a custom persona</Tooltip.Content>
</Tooltip.Root>
</Sheet.Trigger>
<Sheet.Content class="max-w-2xl! gap-0! p-0!">
{#if typeof openFormPersona === 'string'}
<div class="flex items-center gap-3 border-b px-5 py-3">
<Button variant="ghost" size="icon-sm" onclick={() => (openFormPersona = null)}>
<ArrowLeft class="size-4" />
</Button>
<span class="text-sm font-medium">
{openFormPersona === 'new' ? 'New Persona' : 'Edit Persona'}
</span>
</div>
<form
onsubmit={(e) => {
e.preventDefault();
handleSave();
}}
class="flex flex-col gap-5 overflow-y-auto p-5"
>
<div class="flex flex-col gap-1.5">
<label for="persona-name" class="text-sm font-medium">Name</label>
<Input
id="persona-name"
bind:value={formName}
placeholder="e.g. Code Reviewer"
required
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="persona-description" class="text-sm font-medium">
Description
<span class="ml-1 font-normal text-muted-foreground">(optional)</span>
</label>
<Input
id="persona-description"
bind:value={formDescription}
placeholder="A brief summary of what this persona does"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="persona-instructions" class="text-sm font-medium">Instructions</label>
<textarea
id="persona-instructions"
bind:value={formInstructions}
placeholder="You are a helpful code reviewer. You provide concise, actionable feedback..."
rows={7}
class="flex w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
></textarea>
</div>
<Button type="submit" disabled={!formName.trim()} class="self-end">Save Persona</Button>
</form>
{:else}
<Sheet.Header class="mb-0 gap-1! rounded-none border-b p-5">
<Sheet.Title class="flex items-center gap-2">
<Drama class="size-5" />
Personas Manager
</Sheet.Title>
<Sheet.Description
>Use a custom persona to customize the model's behavior.</Sheet.Description
>
</Sheet.Header>
<section class="overflow-y-auto p-5">
<div
class="mb-5 flex items-center gap-2 rounded-md bg-input p-3 text-xs text-muted-foreground"
>
<Info class="size-4 shrink-0" />
<p>
Your custom personas are stored in your browser's local storage. <br />
Meaning, they are not shared with anyone else and are only available on this device.
</p>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
<button
tabindex={-1}
class="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border px-3 py-8 text-muted-foreground transition-all duration-200 hover:bg-accent"
onclick={openNew}
>
<Plus class="size-8" />
<span class="text-sm">New Persona</span>
</button>
{#each [DEFAULT_PERSONA, ...personasState.personas] as persona (persona.id)}
{@const isSelected = personasState.selectedPersona === persona.id}
<div
role="button"
tabindex={-1}
class="group relative flex cursor-pointer flex-col items-start rounded-xl border p-4 text-left transition-all duration-200 {isSelected
? 'border-indigo-500/20 bg-indigo-500/10'
: 'border-border hover:bg-accent '}"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setSelectedPersona(persona.id);
}
}}
onclick={() => setSelectedPersona(persona.id)}
>
{#if isSelected}
<span class="absolute top-2.5 right-2.5 size-1.5 rounded-full bg-indigo-500"></span>
{/if}
<div class="mb-3 flex w-full items-start justify-between gap-2">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-full {persona.avatar
? 'text-lg'
: 'text-sm'} font-semibold {getAvatarClass(persona.name)}"
>
{persona.avatar ?? getInitials(persona.name)}
</div>
{#if persona.id !== DEFAULT_PERSONA.id}
<div
class="flex items-center justify-end gap-0 opacity-0 transition-opacity group-hover:opacity-100"
>
<Button
variant="ghost"
onclick={(e) => {
e.stopPropagation();
openEdit(persona);
}}
tabindex={-1}
size="icon-sm"
class="hover:text-blue-500!"
>
<Pencil class="size-3.5" />
</Button>
<Button
variant="ghost"
tabindex={-1}
onclick={(e) => {
e.stopPropagation();
if (confirm('Are you sure you want to delete this persona?')) {
deletePersona(persona.id);
}
}}
size="icon-sm"
class="hover:text-destructive!"
>
<Trash2 class="size-3.5" />
</Button>
</div>
{/if}
</div>
<p class="text-sm leading-snug font-medium">{persona.name}</p>
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">
{persona.description ?? 'No description'}
</p>
</div>
{/each}
</div>
</section>
{/if}
</Sheet.Content>
</Sheet.Root>