Skip to content

Commit bf41953

Browse files
authored
Bug fixes and improvements (#1104)
- fix image filter - fix json download on firefox - add placeholder help in template editor
1 parent 0a7f37b commit bf41953

File tree

6 files changed

+260
-242
lines changed

6 files changed

+260
-242
lines changed

common/image-util.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function filterImageModels(
66
models: ImageModel[],
77
tier?: Pick<AppSchema.SubscriptionTier, 'imagesAccess'>
88
) {
9+
if (!models) return []
910
if (user?.admin) return models
1011

1112
const list = models.map((m) => ({ ...m, override: '', host: '' }))

web/pages/Character/DownloadModal.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ export const DownloadModal: Component<{
7979
)
8080
)
8181

82+
const objectUrl = createMemo(() => {
83+
const url = URL.createObjectURL(
84+
new Blob([charToJson(props.char || char()!, format())], { type: 'text/json' })
85+
)
86+
return url
87+
})
88+
8289
return (
8390
<Modal
8491
show={props.show && !!char()}
@@ -91,12 +98,7 @@ export const DownloadModal: Component<{
9198
</Button>
9299
<Switch>
93100
<Match when={fileType() === 'json'}>
94-
<a
95-
href={`data:text/json:charset=utf-8,${encodeURIComponent(
96-
charToJson(props.char || char()!, format())
97-
)}`}
98-
download={`${char()!.name}.json`}
99-
>
101+
<a href={objectUrl()} download={`${char()!.name}.json`}>
100102
<Button>
101103
<Save /> Download (JSON)
102104
</Button>
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Component, For, JSX, Match, Switch, createMemo } from 'solid-js'
2+
import { FormLabel } from '../FormLabel'
3+
import { TitleCard } from '../Card'
4+
import { AppSchema } from '/common/types/schema'
5+
import { RootModal } from '../Modal'
6+
import { Interp, InterpAll } from './types'
7+
8+
const helpers: { [key in InterpAll | string]?: JSX.Element | string } = {
9+
char: 'Character name',
10+
user: `Your impersonated character's name. Your profile name if you aren't impersonating a character`,
11+
scenario: `Your main character's scenario`,
12+
personality: `The personality of the replying character`,
13+
example_dialogue: `The example dialogue of the replying character`,
14+
history: `(Required, or use \`#each msgs\`) Aliases: \`messages\` \`msgs\`. Chat history.`,
15+
16+
'json.variable name': 'A value from your JSON schema. E.g. `{{json.name of my value}}`',
17+
system_prompt: `(For instruct models like Turbo, GPT-4, Claude, etc). "Instructions" for how the AI should behave. E.g. "Enter roleplay mode. You will write the {{char}}'s next reply ..."`,
18+
ujb: '(Aka: `{{jailbreak}}`) Similar to `system_prompt`, but typically at the bottom of the prompt',
19+
20+
impersonating: `Your character's personality. This only applies when you are using the "character impersonation" feature.`,
21+
chat_age: `The age of your chat (time elapsed since chat created)`,
22+
idle_duration: `The time elapsed since you last sent a message`,
23+
all_personalities: `Personalities of all characters in the chat EXCEPT the main character.`,
24+
post: 'The "post-amble" text. This gives specific instructions on how the model should respond. E.g. Typically reads: `{{char}}:`',
25+
26+
insert:
27+
"(Aka author's note) Insert text at a specific depth in the prompt. E.g. `{{#insert=4}}This is 4 rows from the bottom{{/insert}}`",
28+
29+
memory: `Text retrieved from your Memory Book(s)`,
30+
31+
longterm_memory:
32+
'(Aka `chat_embed`) Text retrieved from chat history embeddings. Adjust the token budget in the preset `Memory` section.',
33+
user_embed: 'Text retrieved from user-specified embeddings (Articles, PDFs, ...)',
34+
roll: 'Produces a random number. Defaults to "d20". To use a custom number: {{roll [number]}}. E.g.: {{roll 1000}}',
35+
random:
36+
'Produces a random word from a comma-separated list. E.g.: `{{random happy, sad, jealous, angry}}`',
37+
'each bot': (
38+
<>
39+
Supported properties: <code>{`{{.name}} {{.persona}}`}</code>
40+
<br />
41+
Example: <code>{`{{#each bot}}{{.name}}'s personality: {{.persona}}{{/each}}`}</code>
42+
</>
43+
),
44+
'each message': (
45+
<>
46+
{' '}
47+
Supported properties: <code>{`{{.msg}} {{.name}} {{.isuser}} {{.isbot}} {{.i}}`}</code> <br />
48+
You can use <b>conditions</b> for isbot and isuser. E.g.{' '}
49+
<code>{`{{#if .isuser}} ... {{/if}}`}</code>
50+
<br />
51+
Full example:{' '}
52+
<code>{`{{#each msg}}{{#if .isuser}}User: {{.msg}}{{/if}}{{#if .isbot}}Bot: {{.msg}}{{/if}}{{/each}}`}</code>
53+
</>
54+
),
55+
'each chat_embed': (
56+
<>
57+
Supported properties: <code>{`{{.name}} {{.text}}`}</code>
58+
<br />
59+
Example: <code>{`{{#each chat_embed}}{{.name}} said: {{.text}}{{/each}}`}</code>
60+
</>
61+
),
62+
lowpriority: (
63+
<>
64+
Text that is only inserted if there still is token budget remaining for it after inserting
65+
conversation history.
66+
<br />
67+
Example:{' '}
68+
<code>{`{{#if example_dialogue}}{{#lowpriority}}This is how {{char}} speaks: {{example_dialogue}}{{/lowpriority}}{{/if}}`}</code>
69+
</>
70+
),
71+
}
72+
73+
export const DefinitionsModal: Component<{
74+
show: boolean
75+
close: () => void
76+
interps: Interp[]
77+
inherit?: Partial<AppSchema.GenSettings>
78+
}> = (props) => {
79+
const items = createMemo(() => {
80+
const all = Object.entries(helpers)
81+
const entries = all.filter(([interp]) => props.interps.includes(interp as any))
82+
83+
return entries
84+
})
85+
86+
return (
87+
<RootModal
88+
show={props.show}
89+
close={props.close}
90+
title={<div>Placeholder Definitions</div>}
91+
maxWidth="half"
92+
>
93+
<div class="flex w-full flex-col gap-1 text-sm">
94+
<For each={items()}>
95+
{([interp, help]) => (
96+
<TitleCard>
97+
<Switch>
98+
<Match when={typeof help === 'string'}>
99+
<FormLabel label={<b>{interp}</b>} helperMarkdown={help as string} />
100+
</Match>
101+
<Match when>
102+
<FormLabel label={<b>{interp}</b>} helperText={help} />
103+
</Match>
104+
</Switch>
105+
</TitleCard>
106+
)}
107+
</For>
108+
</div>
109+
</RootModal>
110+
)
111+
}

web/shared/PromptEditor/SelectTemplate.tsx

+88-61
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { Component, Match, Switch, createEffect, createMemo, createSignal } from 'solid-js'
22
import { RootModal } from '../Modal'
3-
import { RefreshCcw } from 'lucide-solid'
3+
import { HelpCircle, RefreshCcw } from 'lucide-solid'
44
import Button from '../Button'
55
import { templates } from '../../../common/presets/templates'
66
import Select from '../Select'
77
import TextInput from '../TextInput'
88
import { presetStore, toastStore } from '/web/store'
99
import { PromptSuggestions, onPromptAutoComplete, onPromptKey } from './Suggestions'
10+
import { DefinitionsModal } from './Definitions'
11+
import { Interp, Placeholder, placeholders, v2placeholders } from './types'
12+
import { Pill } from '../Card'
1013

1114
const builtinTemplates = Object.keys(templates).map((key) => ({
1215
label: `(Built-in) ${key}`,
@@ -30,6 +33,7 @@ export const SelectTemplate: Component<{
3033
const [builtin, setBuiltin] = createSignal(templates.Alpaca)
3134
const [filter, setFilter] = createSignal('')
3235
const [autoOpen, setAutoOpen] = createSignal(false)
36+
const [help, showHelp] = createSignal(false)
3337

3438
const templateOpts = createMemo(() => {
3539
const base = Object.entries(templates).reduce(
@@ -89,6 +93,14 @@ export const SelectTemplate: Component<{
8993
return opts.length
9094
}, builtinTemplates.length)
9195

96+
const usable = createMemo(() => {
97+
type Entry = [Interp, Placeholder]
98+
const all = Object.entries(placeholders) as Entry[]
99+
100+
all.push(...(Object.entries(v2placeholders) as Entry[]))
101+
return all
102+
})
103+
92104
const Footer = (
93105
<>
94106
<Button schema="secondary" onClick={props.close}>
@@ -173,69 +185,84 @@ export const SelectTemplate: Component<{
173185
)
174186

175187
return (
176-
<RootModal
177-
title={'Prompt Templates'}
178-
show={props.show}
179-
close={props.close}
180-
footer={Footer}
181-
maxWidth="half"
182-
>
183-
<div class="relative flex flex-col gap-4 text-sm">
184-
<div class="flex gap-1">
188+
<>
189+
<RootModal
190+
title={
191+
<div class="flex gap-1">
192+
Prompt Templates{' '}
193+
<Pill small onClick={() => showHelp(true)}>
194+
Help <HelpCircle class="ml-1" size={16} />
195+
</Pill>
196+
</div>
197+
}
198+
show={props.show}
199+
close={props.close}
200+
footer={Footer}
201+
maxWidth="half"
202+
>
203+
<div class="relative flex flex-col gap-4 text-sm">
204+
<div class="flex gap-1">
205+
<TextInput
206+
fieldName="filter"
207+
placeholder="Filter templates"
208+
onChange={(ev) => setFilter(ev.currentTarget.value)}
209+
parentClass="w-full"
210+
/>
211+
<Button>
212+
<RefreshCcw onClick={presetStore.getTemplates} />
213+
</Button>
214+
</div>
215+
<div class="h-min-[6rem]">
216+
<Select
217+
fieldName="templateId"
218+
items={options().filter((opt) => opt.label.toLowerCase().includes(filter()))}
219+
value={opt()}
220+
onChange={(ev) => {
221+
setOpt(ev.value)
222+
223+
const matches = state.templates.filter((t) => t.name.startsWith(`Custom ${opt()}`))
224+
const name = ev.label.startsWith('(Built-in)')
225+
? matches.length > 0
226+
? `Custom ${opt()} #${matches.length + 1}`
227+
: `Custom ${opt()}`
228+
: templateOpts()[ev.value].name
229+
if (ev.label.startsWith('(Built-in)')) {
230+
}
231+
232+
setName(name)
233+
setTemplate(templateOpts()[ev.value].template)
234+
}}
235+
/>
236+
</div>
237+
<PromptSuggestions
238+
onComplete={(opt) => onPromptAutoComplete(ref, opt)}
239+
open={autoOpen()}
240+
close={() => setAutoOpen(false)}
241+
jsonValues={{ example: '', 'another long example': '', response: '' }}
242+
/>
185243
<TextInput
186-
fieldName="filter"
187-
placeholder="Filter templates"
188-
onChange={(ev) => setFilter(ev.currentTarget.value)}
189-
parentClass="w-full"
244+
fieldName="templateName"
245+
value={templateName()}
246+
onChange={(ev) => setName(ev.currentTarget.value)}
190247
/>
191-
<Button>
192-
<RefreshCcw onClick={presetStore.getTemplates} />
193-
</Button>
194-
</div>
195-
<div class="h-min-[6rem]">
196-
<Select
197-
fieldName="templateId"
198-
items={options().filter((opt) => opt.label.toLowerCase().includes(filter()))}
199-
value={opt()}
200-
onChange={(ev) => {
201-
setOpt(ev.value)
202-
203-
const matches = state.templates.filter((t) => t.name.startsWith(`Custom ${opt()}`))
204-
const name = ev.label.startsWith('(Built-in)')
205-
? matches.length > 0
206-
? `Custom ${opt()} #${matches.length + 1}`
207-
: `Custom ${opt()}`
208-
: templateOpts()[ev.value].name
209-
if (ev.label.startsWith('(Built-in)')) {
210-
}
211-
212-
setName(name)
213-
setTemplate(templateOpts()[ev.value].template)
214-
}}
248+
<TextInput
249+
ref={(r) => (ref = r)}
250+
fieldName="template"
251+
value={template()}
252+
isMultiline
253+
onChange={(ev) => setTemplate(ev.currentTarget.value)}
254+
class="font-mono text-xs"
255+
onKeyDown={(ev) => onPromptKey(ev as any, () => setAutoOpen(true))}
215256
/>
216257
</div>
217-
<PromptSuggestions
218-
onComplete={(opt) => onPromptAutoComplete(ref, opt)}
219-
open={autoOpen()}
220-
close={() => setAutoOpen(false)}
221-
jsonValues={{ example: '', 'another long example': '', response: '' }}
222-
/>
223-
<TextInput
224-
fieldName="templateName"
225-
value={templateName()}
226-
onChange={(ev) => setName(ev.currentTarget.value)}
227-
/>
228-
<TextInput
229-
ref={(r) => (ref = r)}
230-
fieldName="template"
231-
value={template()}
232-
isMultiline
233-
onChange={(ev) => setTemplate(ev.currentTarget.value)}
234-
class="font-mono text-xs"
235-
onKeyDown={(ev) => onPromptKey(ev as any, () => setAutoOpen(true))}
236-
/>
237-
</div>
238-
<div class="flex justify-end gap-2"></div>
239-
</RootModal>
258+
<div class="flex justify-end gap-2"></div>
259+
</RootModal>
260+
261+
<DefinitionsModal
262+
interps={usable().map((item) => item[0])}
263+
show={help()}
264+
close={() => showHelp(false)}
265+
/>
266+
</>
240267
)
241268
}

0 commit comments

Comments
 (0)