enzostvs HF Staff commited on
Commit
4aa9c36
·
1 Parent(s): 7cc4906

Add personas stuffs

Browse files
src/lib/chat/triggerAiCall.ts CHANGED
@@ -1,5 +1,6 @@
1
  import type { ChatMessage, TokenUsage } from '$lib/helpers/types';
2
  import { modelsState } from '$lib/state/models.svelte';
 
3
  import { token } from '$lib/state/token.svelte';
4
  import type { Edge, Node } from '@xyflow/svelte';
5
 
@@ -90,6 +91,11 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
90
  model,
91
  nodeData?.imageData as string | null
92
  );
 
 
 
 
 
93
  const response = await fetch('/api', {
94
  method: 'POST',
95
  body: JSON.stringify({
@@ -97,6 +103,7 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
97
  provider: modelSettings?.provider ?? 'preferred',
98
  messages: formattedMessages,
99
  billingTo: billingOption,
 
100
  ...(modelSettings
101
  ? {
102
  options: {
 
1
  import type { ChatMessage, TokenUsage } from '$lib/helpers/types';
2
  import { modelsState } from '$lib/state/models.svelte';
3
+ import { DEFAULT_PERSONA, personasState } from '$lib/state/personas.svelte';
4
  import { token } from '$lib/state/token.svelte';
5
  import type { Edge, Node } from '@xyflow/svelte';
6
 
 
91
  model,
92
  nodeData?.imageData as string | null
93
  );
94
+ const selectedPersona =
95
+ personasState.selectedPersona === DEFAULT_PERSONA.id
96
+ ? null
97
+ : personasState.personas.find((p) => p.id === personasState.selectedPersona);
98
+
99
  const response = await fetch('/api', {
100
  method: 'POST',
101
  body: JSON.stringify({
 
103
  provider: modelSettings?.provider ?? 'preferred',
104
  messages: formattedMessages,
105
  billingTo: billingOption,
106
+ ...(selectedPersona ? { persona: selectedPersona } : {}),
107
  ...(modelSettings
108
  ? {
109
  options: {
src/lib/components/flow/actions/PanelRightActions.svelte CHANGED
@@ -19,6 +19,7 @@
19
  import InstructionsModal from './InstructionsModal.svelte';
20
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
21
  import * as Tooltip from '$lib/components/ui/tooltip';
 
22
  </script>
23
 
24
  <Panel
@@ -38,6 +39,7 @@
38
  </Tooltip.Trigger>
39
  <Tooltip.Content>Get support or report an issue</Tooltip.Content>
40
  </Tooltip.Root>
 
41
  <InstructionsModal />
42
  <DropdownMenu.Root>
43
  <DropdownMenu.Trigger>
 
19
  import InstructionsModal from './InstructionsModal.svelte';
20
  import { breakpointsState } from '$lib/state/breakpoints.svelte';
21
  import * as Tooltip from '$lib/components/ui/tooltip';
22
+ import PersonasModal from './PersonasModal.svelte';
23
  </script>
24
 
25
  <Panel
 
39
  </Tooltip.Trigger>
40
  <Tooltip.Content>Get support or report an issue</Tooltip.Content>
41
  </Tooltip.Root>
42
+ <PersonasModal />
43
  <InstructionsModal />
44
  <DropdownMenu.Root>
45
  <DropdownMenu.Trigger>
src/lib/components/flow/actions/PersonasModal.svelte ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { ArrowLeft, Drama, Info, Pencil, Plus, Trash2 } from '@lucide/svelte';
3
+ import { Button } from '$lib/components/ui/button';
4
+ import * as Sheet from '$lib/components/ui/sheet/index.js';
5
+ import { breakpointsState } from '$lib/state/breakpoints.svelte';
6
+ import * as Tooltip from '$lib/components/ui/tooltip';
7
+ import {
8
+ personasState,
9
+ addPersona,
10
+ deletePersona,
11
+ savePersona,
12
+ setSelectedPersona,
13
+ DEFAULT_PERSONA
14
+ } from '$lib/state/personas.svelte';
15
+ import { Input } from '$lib/components/ui/input';
16
+ import type { Persona } from '$lib/state/personas.svelte';
17
+
18
+ // bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300
19
+ // bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300
20
+ // bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300
21
+ // bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300
22
+ // bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300
23
+ // bg-slate-100 text-slate-600 dark:bg-slate-700/50 dark:text-slate-300
24
+ const AVATAR_CLASSES = [
25
+ 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300',
26
+ 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
27
+ 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300',
28
+ 'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300',
29
+ 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300',
30
+ 'bg-slate-100 text-slate-600 dark:bg-slate-700/50 dark:text-slate-300'
31
+ ];
32
+
33
+ let open = $state(false);
34
+ let openFormPersona = $state.raw<string | null>(null);
35
+
36
+ let formName = $state('');
37
+ let formDescription = $state('');
38
+ let formInstructions = $state('');
39
+
40
+ function openNew() {
41
+ formName = '';
42
+ formDescription = '';
43
+ formInstructions = '';
44
+ openFormPersona = 'new';
45
+ }
46
+
47
+ function openEdit(persona: Persona) {
48
+ if (persona.id === DEFAULT_PERSONA.id) return;
49
+ formName = persona.name;
50
+ formDescription = persona.description ?? '';
51
+ formInstructions = persona.instructions;
52
+ openFormPersona = persona.id;
53
+ }
54
+
55
+ function handleSave() {
56
+ if (!formName.trim()) return;
57
+ if (openFormPersona === 'new') {
58
+ addPersona({
59
+ name: formName.trim(),
60
+ description: formDescription.trim(),
61
+ instructions: formInstructions.trim()
62
+ });
63
+ } else if (openFormPersona) {
64
+ savePersona(openFormPersona, {
65
+ id: openFormPersona,
66
+ name: formName.trim(),
67
+ description: formDescription.trim(),
68
+ instructions: formInstructions.trim()
69
+ });
70
+ }
71
+ openFormPersona = null;
72
+ }
73
+
74
+ function getInitials(name: string): string {
75
+ return name
76
+ .split(' ')
77
+ .slice(0, 2)
78
+ .map((w) => w[0]?.toUpperCase() ?? '')
79
+ .join('');
80
+ }
81
+
82
+ function getAvatarClass(name: string): string {
83
+ let hash = 0;
84
+ for (let i = 0; i < name.length; i++) {
85
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
86
+ }
87
+ return AVATAR_CLASSES[Math.abs(hash) % AVATAR_CLASSES.length];
88
+ }
89
+ </script>
90
+
91
+ <Sheet.Root bind:open>
92
+ <Sheet.Trigger class="">
93
+ <Tooltip.Root delayDuration={0}>
94
+ <Tooltip.Trigger>
95
+ <Button variant="outline" size={breakpointsState.isMobile ? 'icon-sm' : 'icon-lg'}>
96
+ <Drama class={breakpointsState.isMobile ? 'size-4' : 'size-5'} />
97
+ </Button>
98
+ </Tooltip.Trigger>
99
+ <Tooltip.Content>Use a custom persona</Tooltip.Content>
100
+ </Tooltip.Root>
101
+ </Sheet.Trigger>
102
+
103
+ <Sheet.Content class="max-w-2xl! gap-0! p-0!">
104
+ <Sheet.Header class="mb-0 gap-1! rounded-none border-b p-5">
105
+ <Sheet.Title class="flex items-center gap-2">
106
+ <Drama class="size-5" />
107
+ Personas Manager
108
+ </Sheet.Title>
109
+ <Sheet.Description>Use a custom persona to customize the model's behavior.</Sheet.Description>
110
+ </Sheet.Header>
111
+
112
+ <div class="flex items-center gap-2 bg-input p-3 text-xs text-muted-foreground">
113
+ <Info class="size-4 shrink-0" />
114
+ <p>
115
+ Your custom personas are stored in your browser's local storage. <br />
116
+ Meaning, they are not shared with anyone else and are only available on this device.
117
+ </p>
118
+ </div>
119
+
120
+ {#if typeof openFormPersona === 'string'}
121
+ <div class="flex items-center gap-3 border-b px-5 py-3">
122
+ <Button variant="ghost" size="icon-sm" onclick={() => (openFormPersona = null)}>
123
+ <ArrowLeft class="size-4" />
124
+ </Button>
125
+ <span class="text-sm font-medium">
126
+ {openFormPersona === 'new' ? 'New Persona' : 'Edit Persona'}
127
+ </span>
128
+ </div>
129
+
130
+ <form
131
+ onsubmit={(e) => {
132
+ e.preventDefault();
133
+ handleSave();
134
+ }}
135
+ class="flex flex-col gap-5 overflow-y-auto p-5"
136
+ >
137
+ <div class="flex flex-col gap-1.5">
138
+ <label for="persona-name" class="text-sm font-medium">Name</label>
139
+ <Input
140
+ id="persona-name"
141
+ bind:value={formName}
142
+ placeholder="e.g. Code Reviewer"
143
+ required
144
+ />
145
+ </div>
146
+
147
+ <div class="flex flex-col gap-1.5">
148
+ <label for="persona-description" class="text-sm font-medium">
149
+ Description
150
+ <span class="ml-1 font-normal text-muted-foreground">(optional)</span>
151
+ </label>
152
+ <Input
153
+ id="persona-description"
154
+ bind:value={formDescription}
155
+ placeholder="A brief summary of what this persona does"
156
+ />
157
+ </div>
158
+
159
+ <div class="flex flex-col gap-1.5">
160
+ <label for="persona-instructions" class="text-sm font-medium">Instructions</label>
161
+ <textarea
162
+ id="persona-instructions"
163
+ bind:value={formInstructions}
164
+ placeholder="You are a helpful code reviewer. You provide concise, actionable feedback..."
165
+ rows={7}
166
+ 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"
167
+ ></textarea>
168
+ </div>
169
+
170
+ <Button type="submit" disabled={!formName.trim()} class="self-end">Save Persona</Button>
171
+ </form>
172
+ {:else}
173
+ <section class="overflow-y-auto p-5">
174
+ <div class="grid grid-cols-3 gap-3">
175
+ <button
176
+ tabindex={-1}
177
+ 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"
178
+ onclick={openNew}
179
+ >
180
+ <Plus class="size-8" />
181
+ <span class="text-sm">New Persona</span>
182
+ </button>
183
+
184
+ {#each [DEFAULT_PERSONA, ...personasState.personas] as persona (persona.id)}
185
+ {@const isSelected = personasState.selectedPersona === persona.id}
186
+ <div
187
+ role="button"
188
+ tabindex={-1}
189
+ class="group relative flex cursor-pointer flex-col items-start rounded-xl border p-4 text-left transition-all duration-200 {isSelected
190
+ ? 'border-indigo-500/20 bg-indigo-500/10'
191
+ : 'border-border hover:bg-accent '}"
192
+ onkeydown={(e) => {
193
+ if (e.key === 'Enter' || e.key === ' ') {
194
+ setSelectedPersona(persona.id);
195
+ }
196
+ }}
197
+ onclick={() => setSelectedPersona(persona.id)}
198
+ >
199
+ {#if isSelected}
200
+ <span class="absolute top-2.5 right-2.5 size-1.5 rounded-full bg-indigo-500"></span>
201
+ {/if}
202
+
203
+ <div class="mb-3 flex w-full items-start justify-between gap-2">
204
+ <div
205
+ class="flex size-10 shrink-0 items-center justify-center rounded-full {persona.avatar
206
+ ? 'text-lg'
207
+ : 'text-sm'} font-semibold {getAvatarClass(persona.name)}"
208
+ >
209
+ {persona.avatar ?? getInitials(persona.name)}
210
+ </div>
211
+ {#if persona.id !== DEFAULT_PERSONA.id}
212
+ <div
213
+ class="flex items-center justify-end gap-0 opacity-0 transition-opacity group-hover:opacity-100"
214
+ >
215
+ <Button
216
+ variant="ghost"
217
+ onclick={(e) => {
218
+ e.stopPropagation();
219
+ openEdit(persona);
220
+ }}
221
+ tabindex={-1}
222
+ size="icon-sm"
223
+ class="hover:text-blue-500!"
224
+ >
225
+ <Pencil class="size-3.5" />
226
+ </Button>
227
+ <Button
228
+ variant="ghost"
229
+ tabindex={-1}
230
+ onclick={(e) => {
231
+ e.stopPropagation();
232
+ if (confirm('Are you sure you want to delete this persona?')) {
233
+ deletePersona(persona.id);
234
+ }
235
+ }}
236
+ size="icon-sm"
237
+ class="hover:text-destructive!"
238
+ >
239
+ <Trash2 class="size-3.5" />
240
+ </Button>
241
+ </div>
242
+ {/if}
243
+ </div>
244
+
245
+ <p class="text-sm leading-snug font-medium">{persona.name}</p>
246
+ <p class="mt-1 line-clamp-2 text-xs text-muted-foreground">
247
+ {persona.description ?? 'No description'}
248
+ </p>
249
+ </div>
250
+ {/each}
251
+ </div>
252
+ </section>
253
+ {/if}
254
+ </Sheet.Content>
255
+ </Sheet.Root>
src/lib/components/model/ComboBoxModels.svelte CHANGED
@@ -7,6 +7,7 @@
7
  import { MAX_TRENDING_MODELS } from '$lib';
8
  import Spinner from '../loading/Spinner.svelte';
9
  import ModelImageInput from './ModelImageInput.svelte';
 
10
 
11
  interface Props {
12
  onSelect?: (model: string) => void;
@@ -30,7 +31,7 @@
30
  </script>
31
 
32
  <Button
33
- variant="transparent"
34
  size="icon-sm"
35
  class="!shadow-none!"
36
  onclick={() => {
 
7
  import { MAX_TRENDING_MODELS } from '$lib';
8
  import Spinner from '../loading/Spinner.svelte';
9
  import ModelImageInput from './ModelImageInput.svelte';
10
+ import { mode } from 'mode-watcher';
11
 
12
  interface Props {
13
  onSelect?: (model: string) => void;
 
31
  </script>
32
 
33
  <Button
34
+ variant={mode.current === 'dark' ? 'secondary' : 'outline'}
35
  size="icon-sm"
36
  class="!shadow-none!"
37
  onclick={() => {
src/lib/state/personas.svelte.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const STORAGE_KEY = 'hf-playground-personas';
2
+ const STORAGE_KEY_SELECTED_PERSONA = 'hf-playground-selected-persona';
3
+
4
+ export interface Persona {
5
+ id: string;
6
+ name: string;
7
+ avatar?: string;
8
+ description?: string;
9
+ instructions: string;
10
+ }
11
+
12
+ export const DEFAULT_PERSONA: Persona = {
13
+ id: 'default',
14
+ name: 'Assistant',
15
+ avatar: '🤖',
16
+ description: 'A helpful assistant',
17
+ instructions: 'You are a helpful assistant. You are very helpful and friendly.'
18
+ };
19
+
20
+ export function setSelectedPersona(personaId: string) {
21
+ localStorage.setItem(STORAGE_KEY_SELECTED_PERSONA, personaId);
22
+ personasState.selectedPersona = personaId;
23
+ }
24
+
25
+ function saveStoredPersona(datas: Persona[]) {
26
+ if (typeof window === 'undefined') return;
27
+ try {
28
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(datas));
29
+ } catch {
30
+ // ignore
31
+ }
32
+ }
33
+
34
+ export function addPersona(persona: Omit<Persona, 'id'>) {
35
+ const newPersona: Persona = { ...persona, id: crypto.randomUUID() };
36
+ personasState.personas = [...personasState.personas, newPersona];
37
+ saveStoredPersona(personasState.personas);
38
+ }
39
+
40
+ export function savePersona(personaId: string, datas: Persona) {
41
+ personasState.personas = personasState.personas.map((m) =>
42
+ m.id === personaId ? { ...m, ...datas } : m
43
+ );
44
+ saveStoredPersona(personasState.personas);
45
+ }
46
+
47
+ export function deletePersona(personaId: string) {
48
+ personasState.personas = personasState.personas.filter((p) => p.id !== personaId);
49
+ saveStoredPersona(personasState.personas);
50
+ }
51
+
52
+ export const personasState = $state({
53
+ personas: [] as Persona[],
54
+ selectedPersona: 'default' as string,
55
+ loading: true
56
+ });
57
+
58
+ export async function fetchPersonas() {
59
+ personasState.loading = true;
60
+
61
+ try {
62
+ const personas = localStorage.getItem(STORAGE_KEY);
63
+ const selectedPersona = localStorage.getItem(STORAGE_KEY_SELECTED_PERSONA);
64
+ if (selectedPersona) {
65
+ personasState.selectedPersona = selectedPersona;
66
+ }
67
+ if (!personas) return;
68
+ personasState.personas = JSON.parse(personas) as Persona[];
69
+ } catch {
70
+ personasState.personas = [];
71
+ } finally {
72
+ personasState.loading = false;
73
+ }
74
+ }
src/routes/+layout.svelte CHANGED
@@ -9,6 +9,7 @@
9
  import { initAuth } from '$lib/state/auth.svelte';
10
  import SigninModal from '$lib/components/auth/SigninModal.svelte';
11
  import * as Tooltip from '$lib/components/ui/tooltip/index.js';
 
12
  interface Props {
13
  children?: import('svelte').Snippet;
14
  }
@@ -16,6 +17,7 @@
16
  onMount(() => {
17
  initAuth();
18
  fetchModels();
 
19
  handleBreakpoints();
20
  window.addEventListener('resize', handleBreakpoints);
21
  return () => {
 
9
  import { initAuth } from '$lib/state/auth.svelte';
10
  import SigninModal from '$lib/components/auth/SigninModal.svelte';
11
  import * as Tooltip from '$lib/components/ui/tooltip/index.js';
12
+ import { fetchPersonas } from '$lib/state/personas.svelte';
13
  interface Props {
14
  children?: import('svelte').Snippet;
15
  }
 
17
  onMount(() => {
18
  initAuth();
19
  fetchModels();
20
+ fetchPersonas();
21
  handleBreakpoints();
22
  window.addEventListener('resize', handleBreakpoints);
23
  return () => {
src/routes/api/+server.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { json, type RequestEvent } from '@sveltejs/kit';
2
  import { InferenceClient } from '@huggingface/inference';
 
3
 
4
  export async function POST({ request }: RequestEvent) {
5
  const {
@@ -7,7 +8,8 @@ export async function POST({ request }: RequestEvent) {
7
  messages,
8
  options,
9
  provider = 'preferred',
10
- billingTo = 'personal'
 
11
  } = await request.json();
12
  const token = request.headers.get('Authorization')?.split(' ')[1];
13
 
@@ -36,8 +38,7 @@ export async function POST({ request }: RequestEvent) {
36
  messages: [
37
  {
38
  role: 'system',
39
- content:
40
- "You are a helpful assistant. You are very helpful and friendly. Use markdown to format your responses, but don't include array start and end markers."
41
  },
42
  ...(messages ?? [])
43
  ]
 
1
  import { json, type RequestEvent } from '@sveltejs/kit';
2
  import { InferenceClient } from '@huggingface/inference';
3
+ import { DEFAULT_PERSONA } from '$lib/state/personas.svelte';
4
 
5
  export async function POST({ request }: RequestEvent) {
6
  const {
 
8
  messages,
9
  options,
10
  provider = 'preferred',
11
+ billingTo = 'personal',
12
+ persona = DEFAULT_PERSONA
13
  } = await request.json();
14
  const token = request.headers.get('Authorization')?.split(' ')[1];
15
 
 
38
  messages: [
39
  {
40
  role: 'system',
41
+ content: `Your name is ${persona.name}.\n${persona.instructions} \n\nUse markdown to format your responses, but don't include array start and end markers.`
 
42
  },
43
  ...(messages ?? [])
44
  ]