Spaces:
Running
Running
| <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> | |