feat(persona): role templates for the guided persona builder (#4253, PR2) (#4416)

This commit is contained in:
Mega Mind
2026-07-03 11:19:43 -07:00
committed by GitHub
parent 28331c93a6
commit 30cb552b60
22 changed files with 1144 additions and 25 deletions
@@ -40,6 +40,13 @@ const soulFile = (overrides: Record<string, unknown> = {}) => ({
...overrides,
});
/** Wait for the SOUL section to finish loading (the mode toggle is always shown). */
const awaitLoaded = () =>
waitFor(() => expect(screen.getByTestId('persona-soul-mode-guided')).toBeInTheDocument());
/** Switch to the Advanced (raw markdown) editor. */
const openAdvanced = () => fireEvent.click(screen.getByTestId('persona-soul-mode-advanced'));
describe('PersonaPanel', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -52,17 +59,65 @@ describe('PersonaPanel', () => {
);
});
it('loads SOUL.md contents into the editor on mount', async () => {
it('defaults to the guided builder and hides raw markdown', async () => {
renderWithProviders(<PersonaPanel />);
await waitFor(() => {
expect(screen.getByTestId('persona-soul-editor')).toHaveValue('You are helpful.');
});
await awaitLoaded();
expect(screen.getByTestId('persona-guided-personality')).toBeInTheDocument();
expect(screen.queryByTestId('persona-soul-editor')).not.toBeInTheDocument();
expect(readPersonaFileMock).toHaveBeenCalledWith('SOUL.md');
});
it('reveals the raw SOUL.md editor in Advanced mode', async () => {
renderWithProviders(<PersonaPanel />);
await awaitLoaded();
openAdvanced();
expect(screen.getByTestId('persona-soul-editor')).toHaveValue('You are helpful.');
});
it('splices a guided field edit into SOUL.md and saves it over RPC', async () => {
readPersonaFileMock.mockResolvedValue(
soulFile({ contents: '## Personality\n\nOld.\n', is_default: false })
);
renderWithProviders(<PersonaPanel />);
await awaitLoaded();
await waitFor(() =>
expect(screen.getByTestId('persona-guided-personality')).toHaveValue('Old.')
);
fireEvent.change(screen.getByTestId('persona-guided-personality'), {
target: { value: 'Warm and direct.' },
});
fireEvent.click(screen.getByTestId('persona-soul-save'));
await waitFor(() => {
expect(writePersonaFileMock).toHaveBeenCalledWith(
'SOUL.md',
'## Personality\n\nWarm and direct.\n'
);
});
});
it('applies a role template and saves the seeded persona over RPC', async () => {
readPersonaFileMock.mockResolvedValue(
soulFile({ contents: '## Personality\n\nOld.\n', is_default: false })
);
renderWithProviders(<PersonaPanel />);
await awaitLoaded();
fireEvent.click(screen.getByTestId('persona-template-doctor'));
fireEvent.click(screen.getByTestId('persona-soul-save'));
await waitFor(() => {
const lastCall = writePersonaFileMock.mock.calls.at(-1);
expect(lastCall?.[0]).toBe('SOUL.md');
expect(lastCall?.[1]).toContain('Careful and precise');
expect(lastCall?.[1]).toContain('## Voice');
});
});
it('persists the display name to the store on save', async () => {
const { store } = renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
fireEvent.change(screen.getByTestId('persona-display-name-input'), {
target: { value: 'Nova' },
@@ -78,13 +133,14 @@ describe('PersonaPanel', () => {
it('keeps the identity save button disabled until a field changes', async () => {
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
expect(screen.getByTestId('persona-identity-save')).toBeDisabled();
});
it('writes edited SOUL.md contents over RPC', async () => {
it('writes edited SOUL.md contents over RPC from the raw editor', async () => {
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
openAdvanced();
fireEvent.change(screen.getByTestId('persona-soul-editor'), {
target: { value: 'You are calm and concise.' },
@@ -99,7 +155,8 @@ describe('PersonaPanel', () => {
it('surfaces a save error when the write RPC fails', async () => {
writePersonaFileMock.mockRejectedValue(new Error('disk full'));
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
openAdvanced();
fireEvent.change(screen.getByTestId('persona-soul-editor'), { target: { value: 'edited' } });
fireEvent.click(screen.getByTestId('persona-soul-save'));
@@ -113,7 +170,7 @@ describe('PersonaPanel', () => {
readPersonaFileMock.mockResolvedValue(soulFile({ contents: 'custom', is_default: false }));
resetPersonaFileMock.mockRejectedValue(new Error('reset boom'));
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toHaveValue('custom'));
await awaitLoaded();
fireEvent.click(screen.getByTestId('persona-soul-reset'));
@@ -126,9 +183,9 @@ describe('PersonaPanel', () => {
// Start from a non-default file so the Reset button is enabled.
readPersonaFileMock.mockResolvedValue(soulFile({ contents: 'custom', is_default: false }));
renderWithProviders(<PersonaPanel />);
await waitFor(() => {
expect(screen.getByTestId('persona-soul-editor')).toHaveValue('custom');
});
await awaitLoaded();
openAdvanced();
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toHaveValue('custom'));
fireEvent.click(screen.getByTestId('persona-soul-reset'));
@@ -140,7 +197,7 @@ describe('PersonaPanel', () => {
it('disables Reset while the file is already the bundled default', async () => {
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
expect(screen.getByTestId('persona-soul-reset')).toBeDisabled();
expect(screen.getByTestId('persona-soul-default-badge')).toBeInTheDocument();
});
@@ -155,8 +212,15 @@ describe('PersonaPanel', () => {
it('navigates to the Face tab for avatar & voice', async () => {
renderWithProviders(<PersonaPanel />);
await waitFor(() => expect(screen.getByTestId('persona-soul-editor')).toBeInTheDocument());
await awaitLoaded();
fireEvent.click(screen.getByTestId('persona-open-mascot'));
expect(mockNavigateToSettings).toHaveBeenCalledWith('personality#face');
});
it('links guided users to Agent access for permissions', async () => {
renderWithProviders(<PersonaPanel />);
await awaitLoaded();
fireEvent.click(screen.getByTestId('persona-guided-agent-access'));
expect(mockNavigateToSettings).toHaveBeenCalledWith('agent-access');
});
});
@@ -21,6 +21,9 @@ import Button from '../../ui/Button';
import { SettingsRow, SettingsSection, SettingsTextArea, SettingsTextField } from '../controls';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
import SettingsPanel from '../layout/SettingsPanel';
import PersonaGuidedFields from './persona/PersonaGuidedFields';
type SoulMode = 'guided' | 'advanced';
const log = debug('persona:panel');
@@ -59,6 +62,9 @@ const PersonaPanel = ({ embedded = false }: PersonaPanelProps) => {
const [soulLoading, setSoulLoading] = useState(true);
const [soulError, setSoulError] = useState<string | null>(null);
const [soulBusy, setSoulBusy] = useState(false);
// Guided (structured fields) is the default so users never touch raw markdown;
// Advanced exposes the full SOUL.md text editor for power users.
const [soulMode, setSoulMode] = useState<SoulMode>('guided');
useEffect(() => {
let cancelled = false;
@@ -195,17 +201,44 @@ const PersonaPanel = ({ embedded = false }: PersonaPanelProps) => {
</div>
) : (
<>
<div className="px-4 py-3">
<SettingsTextArea
aria-label={t('settings.persona.soul.editorLabel')}
data-testid="persona-soul-editor"
value={soulDraft}
rows={12}
spellCheck={false}
className="font-mono text-xs leading-relaxed"
onChange={e => setSoulDraft(e.target.value)}
/>
<div
role="group"
aria-label={t('settings.persona.builder.modeLabel')}
className="flex items-center gap-1 px-4 pt-3">
<Button
type="button"
aria-pressed={soulMode === 'guided'}
data-testid="persona-soul-mode-guided"
variant={soulMode === 'guided' ? 'primary' : 'secondary'}
size="xs"
onClick={() => setSoulMode('guided')}>
{t('settings.persona.builder.modeGuided')}
</Button>
<Button
type="button"
aria-pressed={soulMode === 'advanced'}
data-testid="persona-soul-mode-advanced"
variant={soulMode === 'advanced' ? 'primary' : 'secondary'}
size="xs"
onClick={() => setSoulMode('advanced')}>
{t('settings.persona.builder.modeAdvanced')}
</Button>
</div>
{soulMode === 'guided' ? (
<PersonaGuidedFields value={soulDraft} onChange={setSoulDraft} disabled={soulBusy} />
) : (
<div className="px-4 py-3">
<SettingsTextArea
aria-label={t('settings.persona.soul.editorLabel')}
data-testid="persona-soul-editor"
value={soulDraft}
rows={12}
spellCheck={false}
className="font-mono text-xs leading-relaxed"
onChange={e => setSoulDraft(e.target.value)}
/>
</div>
)}
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
<Button
type="button"
@@ -0,0 +1,101 @@
import { useT } from '../../../../lib/i18n/I18nContext';
import { SettingsRow, SettingsTextArea } from '../../controls';
import { useSettingsNavigation } from '../../hooks/useSettingsNavigation';
import { applyPersonaField, parsePersonaFields, type PersonaFieldKey } from './personaSections';
import PersonaTemplatePicker from './PersonaTemplatePicker';
interface PersonaGuidedFieldsProps {
/** The raw SOUL.md text — the single source of truth this view edits. */
value: string;
/** Emit the updated SOUL.md text after a managed section is spliced. */
onChange: (nextSoul: string) => void;
disabled?: boolean;
}
interface FieldDef {
key: PersonaFieldKey;
labelKey: string;
placeholderKey: string;
testId: string;
}
const FIELDS: readonly FieldDef[] = [
{
key: 'personality',
labelKey: 'settings.persona.builder.personalityLabel',
placeholderKey: 'settings.persona.builder.personalityPlaceholder',
testId: 'persona-guided-personality',
},
{
key: 'voice',
labelKey: 'settings.persona.builder.voiceLabel',
placeholderKey: 'settings.persona.builder.voicePlaceholder',
testId: 'persona-guided-voice',
},
{
key: 'about',
labelKey: 'settings.persona.builder.aboutLabel',
placeholderKey: 'settings.persona.builder.aboutPlaceholder',
testId: 'persona-guided-about',
},
] as const;
/**
* Structured persona editor (issue #4253, PR1). Presents a few friendly fields
* that map to named `SOUL.md` sections so non-technical users never touch raw
* markdown. The raw text stays the source of truth: each edit is spliced back
* into `value` via {@link applyPersonaField} and emitted through `onChange`.
*/
const PersonaGuidedFields = ({ value, onChange, disabled = false }: PersonaGuidedFieldsProps) => {
const { t } = useT();
const { navigateToSettings } = useSettingsNavigation();
const fields = parsePersonaFields(value);
return (
<div className="px-4 py-3 space-y-4">
<p className="text-xs text-content-muted leading-relaxed">
{t('settings.persona.builder.intro')}
</p>
<PersonaTemplatePicker value={value} onChange={onChange} disabled={disabled} />
{FIELDS.map(field => (
<SettingsRow
key={field.key}
htmlFor={field.testId}
label={t(field.labelKey)}
stacked
control={
<SettingsTextArea
id={field.testId}
data-testid={field.testId}
aria-label={t(field.labelKey)}
value={fields[field.key]}
rows={3}
disabled={disabled}
placeholder={t(field.placeholderKey)}
onChange={e => onChange(applyPersonaField(value, field.key, e.target.value))}
/>
}
/>
))}
<p className="text-xs text-content-muted leading-relaxed">
{t('settings.persona.builder.preservedNote')}
</p>
<p className="text-xs text-content-muted leading-relaxed">
{t('settings.persona.builder.securityNote')}{' '}
<button
type="button"
data-testid="persona-guided-agent-access"
className="text-primary-700 hover:underline dark:text-primary-300"
onClick={() => navigateToSettings('agent-access')}>
{t('settings.persona.builder.securityLink')}
</button>
</p>
</div>
);
};
export default PersonaGuidedFields;
@@ -0,0 +1,57 @@
import { useT } from '../../../../lib/i18n/I18nContext';
import { applyTemplate, PERSONA_TEMPLATES } from './personaTemplates';
interface PersonaTemplatePickerProps {
/** Current raw SOUL.md text a template is spliced into. */
value: string;
/** Emit the updated SOUL.md text after a template is applied. */
onChange: (nextSoul: string) => void;
disabled?: boolean;
}
/**
* Role starting-points for the guided persona builder (issue #4253, PR2).
*
* Applying a template fills the Personality and Communication-style fields for a
* common role (doctor, researcher, executive, teacher, student, family) and
* leaves the rest of SOUL.md — including the user-specific "About you" — intact.
* Nothing is persisted until the user saves, so this is a safe starting point.
*/
const PersonaTemplatePicker = ({
value,
onChange,
disabled = false,
}: PersonaTemplatePickerProps) => {
const { t } = useT();
return (
<div className="space-y-2">
<div>
<p className="text-sm font-medium text-content">
{t('settings.persona.templates.heading')}
</p>
<p className="text-xs text-content-muted leading-relaxed">
{t('settings.persona.templates.desc')}
</p>
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
{PERSONA_TEMPLATES.map(template => (
<button
key={template.id}
type="button"
disabled={disabled}
data-testid={`persona-template-${template.id}`}
onClick={() => onChange(applyTemplate(value, template))}
className="flex flex-col items-start gap-0.5 rounded-lg border border-line-strong bg-surface px-3 py-2 text-left transition-colors hover:border-primary-400 hover:bg-surface-hover disabled:opacity-50">
<span className="text-sm font-medium text-content">{t(template.labelKey)}</span>
<span className="text-[11px] text-content-muted leading-snug">
{t(template.descriptionKey)}
</span>
</button>
))}
</div>
</div>
);
};
export default PersonaTemplatePicker;
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { applyPersonaField, applyPersonaFields, parsePersonaFields } from './personaSections';
const SOUL = `# OpenHuman
You are OpenHuman.
## Personality
- Warm
- Direct
## Voice
- Lead with the answer.
## When things go wrong
- Own it.
`;
describe('parsePersonaFields', () => {
it('reads the managed sections and leaves About empty when absent', () => {
const fields = parsePersonaFields(SOUL);
expect(fields.personality).toBe('- Warm\n- Direct');
expect(fields.voice).toBe('- Lead with the answer.');
expect(fields.about).toBe('');
});
it('does not match a deeper or differently-named heading', () => {
const text = '## Personality Traits\n\nfoo\n\n### Personality\n\nbar\n';
expect(parsePersonaFields(text).personality).toBe('');
});
it('includes nested h3 content but stops at the next h2', () => {
const text = '## Personality\n\n- a\n### sub\n- b\n\n## Voice\n\nx\n';
expect(parsePersonaFields(text).personality).toBe('- a\n### sub\n- b');
expect(parsePersonaFields(text).voice).toBe('x');
});
});
describe('applyPersonaField', () => {
it('is a no-op (identical string) when the value is unchanged', () => {
expect(applyPersonaField(SOUL, 'personality', '- Warm\n- Direct')).toBe(SOUL);
// trimming differences also count as unchanged
expect(applyPersonaField(SOUL, 'voice', ' - Lead with the answer. ')).toBe(SOUL);
});
it('replaces only the target section and preserves every other byte', () => {
const next = applyPersonaField(SOUL, 'voice', 'Be terse.');
expect(parsePersonaFields(next).voice).toBe('Be terse.');
// untouched sections are byte-identical
expect(next).toContain('## Personality\n\n- Warm\n- Direct');
expect(next).toContain('## When things go wrong\n\n- Own it.');
// and re-applying the original value restores the exact original document
expect(applyPersonaField(next, 'voice', '- Lead with the answer.')).toBe(SOUL);
});
it('appends a new section when the managed heading is absent', () => {
const next = applyPersonaField(SOUL, 'about', 'I design things.');
expect(next.startsWith(SOUL.replace(/\n*$/, '\n'))).toBe(true);
expect(next).toContain('## About You\n\nI design things.\n');
expect(parsePersonaFields(next).about).toBe('I design things.');
});
it('empties the body but keeps the heading when cleared', () => {
const next = applyPersonaField(SOUL, 'voice', '');
expect(parsePersonaFields(next).voice).toBe('');
expect(next).toContain('## Voice');
expect(next).toContain('## When things go wrong');
});
});
describe('applyPersonaFields round-trip', () => {
it('is idempotent when nothing changed', () => {
expect(applyPersonaFields(SOUL, parsePersonaFields(SOUL))).toBe(SOUL);
});
it('round-trips edited fields through parse → apply', () => {
const edited = { personality: 'Calm.', voice: 'Brief.', about: 'A designer.' };
const next = applyPersonaFields(SOUL, edited);
expect(parsePersonaFields(next)).toEqual(edited);
// applying the parsed fields again changes nothing further
expect(applyPersonaFields(next, parsePersonaFields(next))).toBe(next);
});
});
@@ -0,0 +1,129 @@
/**
* SOUL.md ⇄ structured-persona round-trip (issue #4253, PR1).
*
* The guided persona builder edits a handful of named markdown sections inside
* `SOUL.md` without asking the user to write markdown. `SOUL.md` stays the
* single source of truth the assistant runtime reads — we only splice the
* managed sections in place and leave every other byte (the title, the intro,
* and any hand-written sections) untouched. That keeps the round-trip lossless
* and idempotent: parsing then re-applying an unchanged value returns the exact
* same string.
*/
/** Managed field keys the guided builder can edit. */
export type PersonaFieldKey = 'personality' | 'voice' | 'about';
export interface PersonaSectionDef {
key: PersonaFieldKey;
/** Canonical `## ` heading text this field maps to inside SOUL.md. */
heading: string;
}
/**
* The sections the guided builder owns. `Personality` and `Voice` already ship
* in the bundled SOUL.md; `About You` is created on demand the first time the
* user fills it in. Anything not listed here is preserved verbatim.
*/
export const PERSONA_SECTIONS: readonly PersonaSectionDef[] = [
{ key: 'personality', heading: 'Personality' },
{ key: 'voice', heading: 'Voice' },
{ key: 'about', heading: 'About You' },
] as const;
export type PersonaFields = Record<PersonaFieldKey, string>;
const HEADING_FOR: Record<PersonaFieldKey, string> = {
personality: 'Personality',
voice: 'Voice',
about: 'About You',
};
interface SectionSpan {
/** First char of the body (after the heading line's newline). */
bodyStart: number;
/** One past the last char of the body (start of the next `#`/`##` heading, or EOF). */
bodyEnd: number;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Locate a `## <heading>` block and return the char range of its body. Matches
* are case-insensitive and require the heading to be the entire line, so
* `## Personality` matches but `## Personality Traits` and `### Personality` do
* not. The body runs until the next level-1 or level-2 ATX heading (deeper
* `###` headings stay part of the body), or end-of-string.
*/
function findSectionSpan(text: string, heading: string): SectionSpan | null {
const headingRe = new RegExp(`^##[ \\t]+${escapeRegExp(heading)}[ \\t]*$`, 'im');
const match = headingRe.exec(text);
if (!match) return null;
const newlineIdx = text.indexOf('\n', match.index);
const bodyStart = newlineIdx === -1 ? text.length : newlineIdx + 1;
const nextHeadingRe = /^#{1,2}[ \t]/m;
const rest = text.slice(bodyStart);
const nextMatch = nextHeadingRe.exec(rest);
const bodyEnd = nextMatch ? bodyStart + nextMatch.index : text.length;
return { bodyStart, bodyEnd };
}
/** Read the trimmed body of a managed section, or `''` if it is absent. */
function readSection(text: string, heading: string): string {
const span = findSectionSpan(text, heading);
if (!span) return '';
return text.slice(span.bodyStart, span.bodyEnd).trim();
}
/** Parse the managed persona fields out of a SOUL.md document. */
export function parsePersonaFields(soul: string): PersonaFields {
return {
personality: readSection(soul, HEADING_FOR.personality),
voice: readSection(soul, HEADING_FOR.voice),
about: readSection(soul, HEADING_FOR.about),
};
}
/**
* Return a copy of `soul` with a single managed field set to `value`, splicing
* only that section and leaving the rest of the document byte-for-byte intact.
*
* - If the value is unchanged, the original string is returned unchanged.
* - If the section exists, its inner content is replaced while the surrounding
* blank lines are preserved (clean seams, stable diffs).
* - If the section is absent and the value is non-empty, a new `## <heading>`
* block is appended after a single trailing newline.
* - Clearing an existing section empties its body but keeps the heading.
*/
export function applyPersonaField(soul: string, key: PersonaFieldKey, value: string): string {
const heading = HEADING_FOR[key];
const nextBody = value.trim();
if (readSection(soul, heading) === nextBody) return soul;
const span = findSectionSpan(soul, heading);
if (span) {
const raw = soul.slice(span.bodyStart, span.bodyEnd);
const lead = raw.match(/^\n*/)?.[0] ?? '';
const trail = raw.match(/\n*$/)?.[0] ?? '';
const spliced = nextBody ? `${lead}${nextBody}${trail || '\n'}` : `${lead}${trail}`;
return soul.slice(0, span.bodyStart) + spliced + soul.slice(span.bodyEnd);
}
if (!nextBody) return soul;
const base = soul.replace(/\n*$/, '\n');
return `${base}\n## ${heading}\n\n${nextBody}\n`;
}
/** Apply every managed field at once (used for save-all / tests). */
export function applyPersonaFields(soul: string, fields: PersonaFields): string {
let next = soul;
for (const { key } of PERSONA_SECTIONS) {
next = applyPersonaField(next, key, fields[key]);
}
return next;
}
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import { parsePersonaFields } from './personaSections';
import { applyTemplate, PERSONA_TEMPLATES } from './personaTemplates';
const SOUL = `# OpenHuman
You are OpenHuman.
## Personality
- Old trait.
## Voice
- Old voice.
## When things go wrong
- Own it.
`;
describe('PERSONA_TEMPLATES', () => {
it('covers the six requested roles with unique ids and content', () => {
const ids = PERSONA_TEMPLATES.map(t => t.id);
expect(ids).toEqual(['doctor', 'researcher', 'executive', 'teacher', 'student', 'family']);
expect(new Set(ids).size).toBe(ids.length);
for (const tpl of PERSONA_TEMPLATES) {
expect(tpl.fields.personality.trim().length).toBeGreaterThan(0);
expect(tpl.fields.voice.trim().length).toBeGreaterThan(0);
expect(tpl.labelKey).toMatch(/^settings\.persona\.templates\./);
expect(tpl.descriptionKey).toMatch(/^settings\.persona\.templates\./);
}
});
});
describe('applyTemplate', () => {
const doctor = PERSONA_TEMPLATES[0];
it('writes the template Personality and Voice into SOUL.md', () => {
const next = applyTemplate(SOUL, doctor);
const fields = parsePersonaFields(next);
expect(fields.personality).toBe(doctor.fields.personality);
expect(fields.voice).toBe(doctor.fields.voice);
});
it('preserves unmanaged sections and the user-owned About You section', () => {
const withAbout = applyTemplate(SOUL, doctor).replace(
/\n*$/,
'\n\n## About You\n\nI am a nurse.\n'
);
const next = applyTemplate(withAbout, PERSONA_TEMPLATES[2]); // executive
expect(next).toContain('## When things go wrong\n\n- Own it.');
expect(parsePersonaFields(next).about).toBe('I am a nurse.');
expect(parsePersonaFields(next).personality).toBe(PERSONA_TEMPLATES[2].fields.personality);
});
it('is idempotent when the same template is applied twice', () => {
const once = applyTemplate(SOUL, doctor);
expect(applyTemplate(once, doctor)).toBe(once);
});
});
@@ -0,0 +1,128 @@
/**
* Role starting-points for the guided persona builder (issue #4253, PR2).
*
* A template seeds the assistant's *character* — the `Personality` and
* `Communication style` (`Voice`) sections of SOUL.md — for a common role.
* Applying one splices those two sections via {@link applyPersonaField} and
* leaves everything else (including the user-specific `About You` section)
* untouched, so a template is a non-destructive starting point the user then
* edits and saves.
*
* The seed prose is intentionally kept as English constants rather than
* translated i18n: it is written into SOUL.md as editable content (the bundled
* default SOUL.md is English too), not UI chrome. Only the picker's labels and
* descriptions are localized.
*/
import { applyPersonaField } from './personaSections';
export interface PersonaTemplate {
id: string;
labelKey: string;
descriptionKey: string;
fields: { personality: string; voice: string };
}
export const PERSONA_TEMPLATES: readonly PersonaTemplate[] = [
{
id: 'doctor',
labelKey: 'settings.persona.templates.doctor.label',
descriptionKey: 'settings.persona.templates.doctor.desc',
fields: {
personality: [
'- Careful and precise; accuracy always beats speed.',
'- Flags uncertainty openly and never guesses at clinical facts.',
'- Cites sources and reminds the user to verify anything clinical.',
'- Not a medical device: never presents output as diagnosis or treatment.',
].join('\n'),
voice: [
'- Lead with the answer, then the reasoning and caveats.',
'- Use plain, respectful language; define jargon when it helps.',
'- State how confident you are and what would change the answer.',
].join('\n'),
},
},
{
id: 'researcher',
labelKey: 'settings.persona.templates.researcher.label',
descriptionKey: 'settings.persona.templates.researcher.desc',
fields: {
personality: [
'- Rigorous and evidence-first; separate what is known from what is assumed.',
'- Structure findings clearly and keep a list of open questions.',
"- Comfortable saying 'the evidence is thin here.'",
].join('\n'),
voice: [
'- Summarize the finding first, then the detail with sources.',
'- Use precise terms; state assumptions and limitations explicitly.',
].join('\n'),
},
},
{
id: 'executive',
labelKey: 'settings.persona.templates.executive.label',
descriptionKey: 'settings.persona.templates.executive.desc',
fields: {
personality: [
"- Concise and decisive; optimize for the user's time.",
'- Surface the recommendation and the trade-offs, then get out of the way.',
'- Proactive about next steps and who owns them.',
].join('\n'),
voice: [
'- Lead with the bottom line in one sentence.',
'- Bullet the options with clear pros and cons; skip filler.',
].join('\n'),
},
},
{
id: 'teacher',
labelKey: 'settings.persona.templates.teacher.label',
descriptionKey: 'settings.persona.templates.teacher.desc',
fields: {
personality: [
'- Patient and encouraging; meet the learner where they are.',
'- Explain step by step and check understanding along the way.',
'- Turn mistakes into teaching moments.',
].join('\n'),
voice: [
'- Use simple language and concrete examples.',
'- Break problems into small steps and invite questions.',
].join('\n'),
},
},
{
id: 'student',
labelKey: 'settings.persona.templates.student.label',
descriptionKey: 'settings.persona.templates.student.desc',
fields: {
personality: [
'- Encouraging study partner; keep things approachable.',
'- Explain in plain language and quiz to reinforce learning.',
'- Honest when something is beyond what you know.',
].join('\n'),
voice: [
'- Keep it friendly and concrete.',
'- Give a short explanation, then a quick check-question.',
].join('\n'),
},
},
{
id: 'family',
labelKey: 'settings.persona.templates.family.label',
descriptionKey: 'settings.persona.templates.family.desc',
fields: {
personality: [
'- Warm, friendly, and helpful for the whole household.',
'- Keep language simple and kind; suitable for all ages.',
'- Redirect anything unsafe and encourage asking a parent when relevant.',
].join('\n'),
voice: ['- Conversational and clear; avoid jargon.', '- Be brief and positive.'].join('\n'),
},
},
] as const;
/** Splice a template's Personality and Communication-style sections into SOUL.md. */
export function applyTemplate(soul: string, template: PersonaTemplate): string {
let next = applyPersonaField(soul, 'personality', template.fields.personality);
next = applyPersonaField(next, 'voice', template.fields.voice);
return next;
}
+32
View File
@@ -4939,6 +4939,38 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'لا يمكن تحميل Xqx0xx',
'settings.persona.soul.saveError': 'لا يُمكنُ أَنْ يَوفّرَ Xqx0xxx',
'settings.persona.soul.resetError': 'لا يمكن إعادة تشغيل Xqx0xx',
'settings.persona.builder.modeLabel': 'وضع محرر الشخصية',
'settings.persona.builder.modeGuided': 'موجّه',
'settings.persona.builder.modeAdvanced': 'متقدّم',
'settings.persona.builder.intro':
'املأ بضعة حقول وسنكتبها في شخصيتك نيابةً عنك. لا حاجة إلى ماركداون.',
'settings.persona.builder.personalityLabel': 'الشخصية',
'settings.persona.builder.personalityPlaceholder':
'مثال: ودود وفضولي ومباشر. صادق عند عدم اليقين.',
'settings.persona.builder.voiceLabel': 'أسلوب التواصل',
'settings.persona.builder.voicePlaceholder': 'مثال: ابدأ بالإجابة، واجعلها موجزة، وطابق نبرتي.',
'settings.persona.builder.aboutLabel': 'نبذة عنك',
'settings.persona.builder.aboutPlaceholder':
'مثال: أدير استوديو تصميم صغيرًا وأفضّل اللغة البسيطة.',
'settings.persona.builder.preservedNote':
'تُحفظ أي أقسام أخرى كتبتها بنفسك — انتقل إلى «متقدّم» لرؤية الشخصية كاملة.',
'settings.persona.builder.securityNote': 'هل تريد تحديد ما يُسمح للمساعد بفعله؟',
'settings.persona.builder.securityLink': 'فتح وصول الوكيل',
'settings.persona.templates.heading': 'ابدأ من قالب',
'settings.persona.templates.desc':
'اختر نقطة بداية — تملأ الشخصية وأسلوب التواصل. يمكنك تعديل كل شيء لاحقًا.',
'settings.persona.templates.doctor.label': 'مساعد سريري',
'settings.persona.templates.doctor.desc': 'دقيق، يذكر المصادر، ينبّه إلى عدم اليقين',
'settings.persona.templates.researcher.label': 'مساعد بحثي',
'settings.persona.templates.researcher.desc': 'صارم، منظّم، يعتمد على الأدلة',
'settings.persona.templates.executive.label': 'مساعد تنفيذي',
'settings.persona.templates.executive.desc': 'موجز، حاسم، موجّه نحو العمل',
'settings.persona.templates.teacher.label': 'معلّم',
'settings.persona.templates.teacher.desc': 'صبور، يشرح خطوة بخطوة',
'settings.persona.templates.student.label': 'رفيق دراسة',
'settings.persona.templates.student.desc': 'محفّز، يطرح أسئلة، لغة بسيطة',
'settings.persona.templates.family.label': 'مساعد عائلي',
'settings.persona.templates.family.desc': 'ودود، لطيف، آمن لكل الأعمار',
'settings.persona.appearanceHeading': 'صوت الأفاتار',
'settings.persona.appearanceDesc':
'لون الماسكوت، العرف Xqx0x avatar، وصوت الرد مصمم في أماكن مكوت.',
+33
View File
@@ -5043,6 +5043,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'xqxqx লোড করতে ব্যর্থ',
'settings.persona.soul.saveError': 'ছবি সংরক্ষণ করতে ব্যর্থx% 1',
'settings.persona.soul.resetError': 'xqxqx সার্ভার আরম্ভ করতে ব্যর্থ',
'settings.persona.builder.modeLabel': 'পারসোনা এডিটর মোড',
'settings.persona.builder.modeGuided': 'গাইডেড',
'settings.persona.builder.modeAdvanced': 'অ্যাডভান্সড',
'settings.persona.builder.intro':
'কয়েকটি ঘর পূরণ করুন, আমরা সেগুলো আপনার পারসোনায় লিখে দেব। মার্কডাউন লাগবে না।',
'settings.persona.builder.personalityLabel': 'ব্যক্তিত্ব',
'settings.persona.builder.personalityPlaceholder':
'যেমন: উষ্ণ, কৌতূহলী ও স্পষ্টবাদী। অনিশ্চয়তায় সৎ।',
'settings.persona.builder.voiceLabel': 'যোগাযোগের ধরন',
'settings.persona.builder.voicePlaceholder':
'যেমন: আগে উত্তর দিন, সংক্ষিপ্ত রাখুন এবং আমার সুরে মিলিয়ে নিন।',
'settings.persona.builder.aboutLabel': 'আপনার সম্পর্কে',
'settings.persona.builder.aboutPlaceholder':
'যেমন: আমি একটি ছোট ডিজাইন স্টুডিও চালাই এবং সহজ ভাষা পছন্দ করি।',
'settings.persona.builder.preservedNote':
'আপনার নিজের হাতে লেখা অন্যান্য অংশ সংরক্ষিত থাকে — সম্পূর্ণ পারসোনা দেখতে অ্যাডভান্সড-এ যান।',
'settings.persona.builder.securityNote': 'সহকারী কী করতে পারবে তা ঠিক করতে চান?',
'settings.persona.builder.securityLink': 'এজেন্ট অ্যাক্সেস খুলুন',
'settings.persona.templates.heading': 'টেমপ্লেট থেকে শুরু করুন',
'settings.persona.templates.desc':
'একটি শুরুর বিন্দু বেছে নিন — এটি ব্যক্তিত্ব ও যোগাযোগের ধরন পূরণ করে। পরে সব কিছু সম্পাদনা করতে পারবেন।',
'settings.persona.templates.doctor.label': 'ক্লিনিকাল সহকারী',
'settings.persona.templates.doctor.desc': 'সতর্ক, সূত্র উল্লেখ করে, অনিশ্চয়তা চিহ্নিত করে',
'settings.persona.templates.researcher.label': 'গবেষণা সহকারী',
'settings.persona.templates.researcher.desc': 'নিখুঁত, সুসংগঠিত, প্রমাণ-নির্ভর',
'settings.persona.templates.executive.label': 'নির্বাহী সহকারী',
'settings.persona.templates.executive.desc': 'সংক্ষিপ্ত, দৃঢ়, কর্ম-কেন্দ্রিক',
'settings.persona.templates.teacher.label': 'শিক্ষক',
'settings.persona.templates.teacher.desc': 'ধৈর্যশীল, ধাপে ধাপে ব্যাখ্যা করে',
'settings.persona.templates.student.label': 'পড়ার সঙ্গী',
'settings.persona.templates.student.desc': 'উৎসাহদায়ক, কুইজ করে, সহজ ভাষা',
'settings.persona.templates.family.label': 'পারিবারিক সহকারী',
'settings.persona.templates.family.desc': 'উষ্ণ, বন্ধুত্বপূর্ণ, সব বয়সের জন্য নিরাপদ',
'settings.persona.appearanceHeading': 'অবতার & ভয়েস',
'settings.persona.appearanceDesc':
'Mascot রঙের রং, স্বনির্ধারিত xxqxqx অ্যাভাটার, এবং Scotox বৈশিষ্ট্যের মধ্য থেকে ভয়েস কনফিগার করা হয়েছে।',
+33
View File
@@ -5177,6 +5177,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'SOUL.md konnte nicht geladen werden',
'settings.persona.soul.saveError': 'SOUL.md konnte nicht gespeichert werden',
'settings.persona.soul.resetError': 'SOUL.md konnte nicht zurückgesetzt werden',
'settings.persona.builder.modeLabel': 'Persona-Editor-Modus',
'settings.persona.builder.modeGuided': 'Geführt',
'settings.persona.builder.modeAdvanced': 'Erweitert',
'settings.persona.builder.intro':
'Füllen Sie ein paar Felder aus und wir schreiben sie für Sie in Ihre Persona. Kein Markdown nötig.',
'settings.persona.builder.personalityLabel': 'Persönlichkeit',
'settings.persona.builder.personalityPlaceholder':
'z. B. Warmherzig, neugierig und direkt. Ehrlich bei Unsicherheit.',
'settings.persona.builder.voiceLabel': 'Kommunikationsstil',
'settings.persona.builder.voicePlaceholder':
'z. B. Zuerst die Antwort, kurz halten und meinen Ton treffen.',
'settings.persona.builder.aboutLabel': 'Über Sie',
'settings.persona.builder.aboutPlaceholder':
'z. B. Ich leite ein kleines Designstudio und bevorzuge klare Sprache.',
'settings.persona.builder.preservedNote':
'Alle anderen von Hand geschriebenen Abschnitte bleiben erhalten — wechseln Sie zu „Erweitert“, um die vollständige Persona zu sehen.',
'settings.persona.builder.securityNote': 'Möchten Sie festlegen, was der Assistent tun darf?',
'settings.persona.builder.securityLink': 'Agentenzugriff öffnen',
'settings.persona.templates.heading': 'Mit einer Vorlage beginnen',
'settings.persona.templates.desc':
'Wählen Sie einen Ausgangspunkt — er füllt Persönlichkeit und Kommunikationsstil. Danach können Sie alles bearbeiten.',
'settings.persona.templates.doctor.label': 'Klinischer Assistent',
'settings.persona.templates.doctor.desc': 'Sorgfältig, nennt Quellen, weist auf Unsicherheit hin',
'settings.persona.templates.researcher.label': 'Forschungsassistent',
'settings.persona.templates.researcher.desc': 'Gründlich, strukturiert, evidenzbasiert',
'settings.persona.templates.executive.label': 'Assistent für Führungskräfte',
'settings.persona.templates.executive.desc': 'Prägnant, entschlossen, handlungsorientiert',
'settings.persona.templates.teacher.label': 'Lehrkraft',
'settings.persona.templates.teacher.desc': 'Geduldig, erklärt Schritt für Schritt',
'settings.persona.templates.student.label': 'Lernbegleiter',
'settings.persona.templates.student.desc': 'Ermutigend, stellt Quizfragen, einfache Sprache',
'settings.persona.templates.family.label': 'Familienassistent',
'settings.persona.templates.family.desc': 'Herzlich, freundlich, für alle Altersgruppen geeignet',
'settings.persona.appearanceHeading': 'Avatar und Stimme',
'settings.persona.appearanceDesc':
'Maskottchenfarbe, benutzerdefinierter GIF-Avatar und Antwortstimme werden in den Maskottcheneinstellungen konfiguriert.',
+33
View File
@@ -5683,6 +5683,39 @@ const en: TranslationMap = {
'settings.persona.soul.loadError': 'Could not load SOUL.md',
'settings.persona.soul.saveError': 'Could not save SOUL.md',
'settings.persona.soul.resetError': 'Could not reset SOUL.md',
'settings.persona.builder.modeLabel': 'Persona editor mode',
'settings.persona.builder.modeGuided': 'Guided',
'settings.persona.builder.modeAdvanced': 'Advanced',
'settings.persona.builder.intro':
'Fill in a few fields and we write them into your persona for you. No markdown required.',
'settings.persona.builder.personalityLabel': 'Personality',
'settings.persona.builder.personalityPlaceholder':
'e.g. Warm, curious, and direct. Honest about uncertainty.',
'settings.persona.builder.voiceLabel': 'Communication style',
'settings.persona.builder.voicePlaceholder':
'e.g. Lead with the answer, keep it brief, and match my tone.',
'settings.persona.builder.aboutLabel': 'About you',
'settings.persona.builder.aboutPlaceholder':
'e.g. I run a small design studio and prefer plain language.',
'settings.persona.builder.preservedNote':
'Any other sections you wrote by hand are kept — switch to Advanced to see the full persona.',
'settings.persona.builder.securityNote': 'Choosing what the assistant is allowed to do?',
'settings.persona.builder.securityLink': 'Open Agent access',
'settings.persona.templates.heading': 'Start from a template',
'settings.persona.templates.desc':
'Pick a starting point — it fills Personality and Communication style. You can edit everything afterwards.',
'settings.persona.templates.doctor.label': 'Clinical assistant',
'settings.persona.templates.doctor.desc': 'Careful, cites sources, flags uncertainty',
'settings.persona.templates.researcher.label': 'Research assistant',
'settings.persona.templates.researcher.desc': 'Rigorous, structured, evidence-first',
'settings.persona.templates.executive.label': 'Executive assistant',
'settings.persona.templates.executive.desc': 'Concise, decisive, action-oriented',
'settings.persona.templates.teacher.label': 'Teacher',
'settings.persona.templates.teacher.desc': 'Patient, explains step by step',
'settings.persona.templates.student.label': 'Study buddy',
'settings.persona.templates.student.desc': 'Encouraging, quizzes, plain language',
'settings.persona.templates.family.label': 'Family assistant',
'settings.persona.templates.family.desc': 'Warm, friendly, safe for all ages',
'settings.persona.appearanceHeading': 'Avatar & Voice',
'settings.persona.appearanceDesc':
'Mascot color, custom GIF avatar, and reply voice are configured in Mascot settings.',
+33
View File
@@ -5140,6 +5140,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'No se pudo cargar SOUL.md',
'settings.persona.soul.saveError': 'No se pudo guardar SOUL.md',
'settings.persona.soul.resetError': 'No se pudo restablecer SOUL.md',
'settings.persona.builder.modeLabel': 'Modo del editor de persona',
'settings.persona.builder.modeGuided': 'Guiado',
'settings.persona.builder.modeAdvanced': 'Avanzado',
'settings.persona.builder.intro':
'Rellena unos campos y los escribimos en tu persona por ti. No hace falta markdown.',
'settings.persona.builder.personalityLabel': 'Personalidad',
'settings.persona.builder.personalityPlaceholder':
'p. ej. Cercano, curioso y directo. Honesto ante la incertidumbre.',
'settings.persona.builder.voiceLabel': 'Estilo de comunicación',
'settings.persona.builder.voicePlaceholder':
'p. ej. Empieza por la respuesta, sé breve y adapta mi tono.',
'settings.persona.builder.aboutLabel': 'Sobre ti',
'settings.persona.builder.aboutPlaceholder':
'p. ej. Dirijo un pequeño estudio de diseño y prefiero un lenguaje sencillo.',
'settings.persona.builder.preservedNote':
'Cualquier otra sección que hayas escrito a mano se conserva: cambia a Avanzado para ver la persona completa.',
'settings.persona.builder.securityNote': '¿Quieres elegir lo que el asistente puede hacer?',
'settings.persona.builder.securityLink': 'Abrir Acceso del agente',
'settings.persona.templates.heading': 'Empezar con una plantilla',
'settings.persona.templates.desc':
'Elige un punto de partida: rellena Personalidad y Estilo de comunicación. Después puedes editarlo todo.',
'settings.persona.templates.doctor.label': 'Asistente clínico',
'settings.persona.templates.doctor.desc': 'Cuidadoso, cita fuentes, señala la incertidumbre',
'settings.persona.templates.researcher.label': 'Asistente de investigación',
'settings.persona.templates.researcher.desc': 'Riguroso, estructurado, basado en evidencia',
'settings.persona.templates.executive.label': 'Asistente ejecutivo',
'settings.persona.templates.executive.desc': 'Conciso, decidido, orientado a la acción',
'settings.persona.templates.teacher.label': 'Docente',
'settings.persona.templates.teacher.desc': 'Paciente, explica paso a paso',
'settings.persona.templates.student.label': 'Compañero de estudio',
'settings.persona.templates.student.desc': 'Alentador, hace preguntas, lenguaje sencillo',
'settings.persona.templates.family.label': 'Asistente familiar',
'settings.persona.templates.family.desc': 'Cálido, amable, seguro para todas las edades',
'settings.persona.appearanceHeading': 'Avatar y Voz',
'settings.persona.appearanceDesc':
'El color de la mascota, el avatar personalizado GIF y la voz de respuesta se configuran en los ajustes de la mascota.',
+34
View File
@@ -5158,6 +5158,40 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Impossible de charger SOUL.md',
'settings.persona.soul.saveError': "Impossible d'enregistrer SOUL.md",
'settings.persona.soul.resetError': 'Impossible de réinitialiser SOUL.md',
'settings.persona.builder.modeLabel': 'Mode de l’éditeur de persona',
'settings.persona.builder.modeGuided': 'Guidé',
'settings.persona.builder.modeAdvanced': 'Avancé',
'settings.persona.builder.intro':
'Remplissez quelques champs et nous les écrivons dans votre persona. Aucun markdown requis.',
'settings.persona.builder.personalityLabel': 'Personnalité',
'settings.persona.builder.personalityPlaceholder':
'p. ex. Chaleureux, curieux et direct. Honnête face à lincertitude.',
'settings.persona.builder.voiceLabel': 'Style de communication',
'settings.persona.builder.voicePlaceholder':
'p. ex. Commencez par la réponse, restez bref et adaptez mon ton.',
'settings.persona.builder.aboutLabel': 'À propos de vous',
'settings.persona.builder.aboutPlaceholder':
'p. ex. Je dirige un petit studio de design et je préfère un langage simple.',
'settings.persona.builder.preservedNote':
'Toutes les autres sections que vous avez écrites à la main sont conservées — passez en mode Avancé pour voir la persona complète.',
'settings.persona.builder.securityNote':
'Vous voulez choisir ce que lassistant est autorisé à faire ?',
'settings.persona.builder.securityLink': 'Ouvrir Accès de lagent',
'settings.persona.templates.heading': 'Partir dun modèle',
'settings.persona.templates.desc':
'Choisissez un point de départ : il remplit Personnalité et Style de communication. Vous pourrez tout modifier ensuite.',
'settings.persona.templates.doctor.label': 'Assistant clinique',
'settings.persona.templates.doctor.desc': 'Prudent, cite ses sources, signale lincertitude',
'settings.persona.templates.researcher.label': 'Assistant de recherche',
'settings.persona.templates.researcher.desc': 'Rigoureux, structuré, fondé sur des preuves',
'settings.persona.templates.executive.label': 'Assistant de direction',
'settings.persona.templates.executive.desc': 'Concis, décisif, orienté action',
'settings.persona.templates.teacher.label': 'Enseignant',
'settings.persona.templates.teacher.desc': 'Patient, explique étape par étape',
'settings.persona.templates.student.label': 'Partenaire d’étude',
'settings.persona.templates.student.desc': 'Encourageant, pose des questions, langage simple',
'settings.persona.templates.family.label': 'Assistant familial',
'settings.persona.templates.family.desc': 'Chaleureux, amical, adapté à tous les âges',
'settings.persona.appearanceHeading': 'Avatar et Voix',
'settings.persona.appearanceDesc':
"La couleur de la mascotte, l'avatar personnalisé GIF et la voix de réponse sont configurés dans les paramètres de la mascotte.",
+33
View File
@@ -5046,6 +5046,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'SOUL',
'settings.persona.soul.saveError': 'नहीं बचा सकता SOUL.md',
'settings.persona.soul.resetError': 'SOUL.md रीसेट नहीं कर सका',
'settings.persona.builder.modeLabel': 'पर्सोना एडिटर मोड',
'settings.persona.builder.modeGuided': 'निर्देशित',
'settings.persona.builder.modeAdvanced': 'उन्नत',
'settings.persona.builder.intro':
'कुछ फ़ील्ड भरें और हम उन्हें आपके पर्सोना में लिख देंगे। मार्कडाउन की ज़रूरत नहीं।',
'settings.persona.builder.personalityLabel': 'व्यक्तित्व',
'settings.persona.builder.personalityPlaceholder':
'जैसे: गर्मजोश, जिज्ञासु और सीधा। अनिश्चितता पर ईमानदार।',
'settings.persona.builder.voiceLabel': 'संवाद शैली',
'settings.persona.builder.voicePlaceholder':
'जैसे: पहले उत्तर दें, संक्षिप्त रखें और मेरे लहजे से मेल खाएँ।',
'settings.persona.builder.aboutLabel': 'आपके बारे में',
'settings.persona.builder.aboutPlaceholder':
'जैसे: मैं एक छोटा डिज़ाइन स्टूडियो चलाता हूँ और सरल भाषा पसंद करता हूँ।',
'settings.persona.builder.preservedNote':
'आपके हाथ से लिखे अन्य अनुभाग सुरक्षित रहते हैं — पूरा पर्सोना देखने के लिए उन्नत पर जाएँ।',
'settings.persona.builder.securityNote': 'तय करना चाहते हैं कि असिस्टेंट क्या कर सकता है?',
'settings.persona.builder.securityLink': 'एजेंट एक्सेस खोलें',
'settings.persona.templates.heading': 'टेम्पलेट से शुरू करें',
'settings.persona.templates.desc':
'एक शुरुआती बिंदु चुनें — यह व्यक्तित्व और संवाद शैली भर देता है। बाद में आप सब कुछ बदल सकते हैं।',
'settings.persona.templates.doctor.label': 'क्लिनिकल सहायक',
'settings.persona.templates.doctor.desc': 'सावधान, स्रोत बताता है, अनिश्चितता दर्शाता है',
'settings.persona.templates.researcher.label': 'शोध सहायक',
'settings.persona.templates.researcher.desc': 'सटीक, व्यवस्थित, साक्ष्य-आधारित',
'settings.persona.templates.executive.label': 'कार्यकारी सहायक',
'settings.persona.templates.executive.desc': 'संक्षिप्त, निर्णायक, कार्य-केंद्रित',
'settings.persona.templates.teacher.label': 'शिक्षक',
'settings.persona.templates.teacher.desc': 'धैर्यवान, कदम-दर-कदम समझाता है',
'settings.persona.templates.student.label': 'अध्ययन साथी',
'settings.persona.templates.student.desc': 'प्रोत्साहित करता है, सवाल पूछता है, सरल भाषा',
'settings.persona.templates.family.label': 'पारिवारिक सहायक',
'settings.persona.templates.family.desc': 'गर्मजोश, मिलनसार, हर उम्र के लिए सुरक्षित',
'settings.persona.appearanceHeading': 'अवतार और आवाज',
'settings.persona.appearanceDesc':
'Mascot रंग, कस्टम GIF अवतार, और उत्तर आवाज Mascot सेटिंग्स में कॉन्फ़िगर किया गया है।',
+33
View File
@@ -5061,6 +5061,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Tidak dapat memuat SOUL.md',
'settings.persona.soul.saveError': 'Tidak dapat menyimpan SOUL.md',
'settings.persona.soul.resetError': 'Tidak dapat mereset SOUL.md',
'settings.persona.builder.modeLabel': 'Mode editor persona',
'settings.persona.builder.modeGuided': 'Terpandu',
'settings.persona.builder.modeAdvanced': 'Lanjutan',
'settings.persona.builder.intro':
'Isi beberapa kolom dan kami menuliskannya ke persona Anda. Tanpa markdown.',
'settings.persona.builder.personalityLabel': 'Kepribadian',
'settings.persona.builder.personalityPlaceholder':
'mis. Hangat, ingin tahu, dan langsung. Jujur soal ketidakpastian.',
'settings.persona.builder.voiceLabel': 'Gaya komunikasi',
'settings.persona.builder.voicePlaceholder':
'mis. Mulai dari jawaban, singkat, dan sesuaikan dengan nada saya.',
'settings.persona.builder.aboutLabel': 'Tentang Anda',
'settings.persona.builder.aboutPlaceholder':
'mis. Saya menjalankan studio desain kecil dan lebih suka bahasa yang sederhana.',
'settings.persona.builder.preservedNote':
'Bagian lain yang Anda tulis sendiri tetap disimpan — beralih ke Lanjutan untuk melihat persona lengkap.',
'settings.persona.builder.securityNote': 'Ingin memilih apa yang boleh dilakukan asisten?',
'settings.persona.builder.securityLink': 'Buka Akses agen',
'settings.persona.templates.heading': 'Mulai dari templat',
'settings.persona.templates.desc':
'Pilih titik awal — mengisi Kepribadian dan Gaya komunikasi. Anda bisa mengedit semuanya setelahnya.',
'settings.persona.templates.doctor.label': 'Asisten klinis',
'settings.persona.templates.doctor.desc': 'Cermat, menyebut sumber, menandai ketidakpastian',
'settings.persona.templates.researcher.label': 'Asisten riset',
'settings.persona.templates.researcher.desc': 'Teliti, terstruktur, berbasis bukti',
'settings.persona.templates.executive.label': 'Asisten eksekutif',
'settings.persona.templates.executive.desc': 'Ringkas, tegas, berorientasi tindakan',
'settings.persona.templates.teacher.label': 'Guru',
'settings.persona.templates.teacher.desc': 'Sabar, menjelaskan langkah demi langkah',
'settings.persona.templates.student.label': 'Teman belajar',
'settings.persona.templates.student.desc': 'Menyemangati, memberi kuis, bahasa sederhana',
'settings.persona.templates.family.label': 'Asisten keluarga',
'settings.persona.templates.family.desc': 'Hangat, ramah, aman untuk segala usia',
'settings.persona.appearanceHeading': 'Avatar & Suara',
'settings.persona.appearanceDesc':
'Warna Mascot, avatar GIF kustom, dan suara balasan dikonfigurasi dalam pengaturan Mascot.',
+33
View File
@@ -5130,6 +5130,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Impossibile caricare SOUL.md',
'settings.persona.soul.saveError': 'Impossibile salvare SOUL.md',
'settings.persona.soul.resetError': 'Impossibile reimpostare SOUL.md',
'settings.persona.builder.modeLabel': 'Modalità editor della persona',
'settings.persona.builder.modeGuided': 'Guidata',
'settings.persona.builder.modeAdvanced': 'Avanzata',
'settings.persona.builder.intro':
'Compila alcuni campi e li scriviamo noi nella tua persona. Nessun markdown richiesto.',
'settings.persona.builder.personalityLabel': 'Personalità',
'settings.persona.builder.personalityPlaceholder':
'es. Cordiale, curioso e diretto. Onesto sullincertezza.',
'settings.persona.builder.voiceLabel': 'Stile di comunicazione',
'settings.persona.builder.voicePlaceholder':
'es. Inizia dalla risposta, sii breve e adatta il mio tono.',
'settings.persona.builder.aboutLabel': 'Su di te',
'settings.persona.builder.aboutPlaceholder':
'es. Gestisco un piccolo studio di design e preferisco un linguaggio semplice.',
'settings.persona.builder.preservedNote':
'Le altre sezioni che hai scritto a mano vengono mantenute: passa ad Avanzata per vedere la persona completa.',
'settings.persona.builder.securityNote': 'Vuoi scegliere cosa può fare lassistente?',
'settings.persona.builder.securityLink': 'Apri Accesso agente',
'settings.persona.templates.heading': 'Parti da un modello',
'settings.persona.templates.desc':
'Scegli un punto di partenza: compila Personalità e Stile di comunicazione. Dopo puoi modificare tutto.',
'settings.persona.templates.doctor.label': 'Assistente clinico',
'settings.persona.templates.doctor.desc': 'Attento, cita le fonti, segnala lincertezza',
'settings.persona.templates.researcher.label': 'Assistente di ricerca',
'settings.persona.templates.researcher.desc': 'Rigoroso, strutturato, basato sulle prove',
'settings.persona.templates.executive.label': 'Assistente esecutivo',
'settings.persona.templates.executive.desc': 'Conciso, deciso, orientato allazione',
'settings.persona.templates.teacher.label': 'Insegnante',
'settings.persona.templates.teacher.desc': 'Paziente, spiega passo dopo passo',
'settings.persona.templates.student.label': 'Compagno di studio',
'settings.persona.templates.student.desc': 'Incoraggiante, fa domande, linguaggio semplice',
'settings.persona.templates.family.label': 'Assistente per la famiglia',
'settings.persona.templates.family.desc': 'Caloroso, amichevole, adatto a tutte le età',
'settings.persona.appearanceHeading': 'Avatar e Voce',
'settings.persona.appearanceDesc':
"Il colore della mascotte, l'avatar personalizzato GIF e la voce di risposta sono configurati nelle impostazioni della mascotte.",
+32
View File
@@ -4992,6 +4992,38 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'SOUL.md를 불러올 수 없습니다',
'settings.persona.soul.saveError': 'SOUL.md를 저장할 수 없습니다',
'settings.persona.soul.resetError': 'SOUL.md를 초기화할 수 없습니다',
'settings.persona.builder.modeLabel': '페르소나 편집기 모드',
'settings.persona.builder.modeGuided': '가이드',
'settings.persona.builder.modeAdvanced': '고급',
'settings.persona.builder.intro':
'몇 가지 항목만 입력하면 페르소나에 대신 작성해 드립니다. 마크다운은 필요 없습니다.',
'settings.persona.builder.personalityLabel': '성격',
'settings.persona.builder.personalityPlaceholder':
'예: 따뜻하고 호기심 많고 직설적. 불확실할 땐 솔직하게.',
'settings.persona.builder.voiceLabel': '커뮤니케이션 스타일',
'settings.persona.builder.voicePlaceholder': '예: 답부터 말하고, 간결하게, 내 말투에 맞춰서.',
'settings.persona.builder.aboutLabel': '당신에 대해',
'settings.persona.builder.aboutPlaceholder':
'예: 작은 디자인 스튜디오를 운영하며 쉬운 표현을 선호합니다.',
'settings.persona.builder.preservedNote':
'직접 작성한 다른 섹션은 그대로 유지됩니다 — 전체 페르소나를 보려면 고급으로 전환하세요.',
'settings.persona.builder.securityNote': '어시스턴트가 할 수 있는 일을 정하시겠어요?',
'settings.persona.builder.securityLink': '에이전트 액세스 열기',
'settings.persona.templates.heading': '템플릿으로 시작',
'settings.persona.templates.desc':
'시작점을 고르세요 — 성격과 커뮤니케이션 스타일을 채워 줍니다. 이후 모두 수정할 수 있습니다.',
'settings.persona.templates.doctor.label': '임상 어시스턴트',
'settings.persona.templates.doctor.desc': '신중하고 출처를 밝히며 불확실성을 표시',
'settings.persona.templates.researcher.label': '리서치 어시스턴트',
'settings.persona.templates.researcher.desc': '엄밀하고 체계적이며 근거 우선',
'settings.persona.templates.executive.label': '임원 어시스턴트',
'settings.persona.templates.executive.desc': '간결하고 결단력 있으며 실행 중심',
'settings.persona.templates.teacher.label': '교사',
'settings.persona.templates.teacher.desc': '인내심 있게 단계별로 설명',
'settings.persona.templates.student.label': '공부 친구',
'settings.persona.templates.student.desc': '격려하고 퀴즈를 내며 쉬운 언어 사용',
'settings.persona.templates.family.label': '가족 어시스턴트',
'settings.persona.templates.family.desc': '따뜻하고 친근하며 모든 연령에 안전',
'settings.persona.appearanceHeading': '아바타 및 음성',
'settings.persona.appearanceDesc':
'마스코트 색상, 사용자 지정 GIF 아바타, 응답 음성은 마스코트 설정에서 구성합니다.',
+33
View File
@@ -5118,6 +5118,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Nie udało się wczytać SOUL.md',
'settings.persona.soul.saveError': 'Nie udało się zapisać SOUL.md',
'settings.persona.soul.resetError': 'Nie udało się zresetować SOUL.md',
'settings.persona.builder.modeLabel': 'Tryb edytora persony',
'settings.persona.builder.modeGuided': 'Prowadzony',
'settings.persona.builder.modeAdvanced': 'Zaawansowany',
'settings.persona.builder.intro':
'Wypełnij kilka pól, a my zapiszemy je w Twojej personie. Markdown nie jest potrzebny.',
'settings.persona.builder.personalityLabel': 'Osobowość',
'settings.persona.builder.personalityPlaceholder':
'np. Ciepły, ciekawy i bezpośredni. Szczery wobec niepewności.',
'settings.persona.builder.voiceLabel': 'Styl komunikacji',
'settings.persona.builder.voicePlaceholder':
'np. Zacznij od odpowiedzi, pisz zwięźle i dopasuj mój ton.',
'settings.persona.builder.aboutLabel': 'O Tobie',
'settings.persona.builder.aboutPlaceholder':
'np. Prowadzę małe studio projektowe i wolę prosty język.',
'settings.persona.builder.preservedNote':
'Wszelkie inne sekcje napisane ręcznie zostają zachowane — przełącz na Zaawansowany, aby zobaczyć pełną personę.',
'settings.persona.builder.securityNote': 'Chcesz wybrać, co asystent może robić?',
'settings.persona.builder.securityLink': 'Otwórz Dostęp agenta',
'settings.persona.templates.heading': 'Zacznij od szablonu',
'settings.persona.templates.desc':
'Wybierz punkt wyjścia — wypełni Osobowość i Styl komunikacji. Wszystko możesz później edytować.',
'settings.persona.templates.doctor.label': 'Asystent kliniczny',
'settings.persona.templates.doctor.desc': 'Uważny, podaje źródła, sygnalizuje niepewność',
'settings.persona.templates.researcher.label': 'Asystent badawczy',
'settings.persona.templates.researcher.desc': 'Rzetelny, uporządkowany, oparty na dowodach',
'settings.persona.templates.executive.label': 'Asystent kierownictwa',
'settings.persona.templates.executive.desc': 'Zwięzły, zdecydowany, nastawiony na działanie',
'settings.persona.templates.teacher.label': 'Nauczyciel',
'settings.persona.templates.teacher.desc': 'Cierpliwy, tłumaczy krok po kroku',
'settings.persona.templates.student.label': 'Partner do nauki',
'settings.persona.templates.student.desc': 'Zachęcający, zadaje pytania, prosty język',
'settings.persona.templates.family.label': 'Asystent rodzinny',
'settings.persona.templates.family.desc': 'Ciepły, przyjazny, bezpieczny dla każdego wieku',
'settings.persona.appearanceHeading': 'Awatar i głos',
'settings.persona.appearanceDesc':
'Kolor maskotki, własny awatar GIF i głos odpowiedzi są konfigurowane w ustawieniach Maskotki.',
+33
View File
@@ -5132,6 +5132,39 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Não foi possível carregar SOUL.md',
'settings.persona.soul.saveError': 'Não foi possível salvar SOUL.md',
'settings.persona.soul.resetError': 'Não foi possível resetar SOUL.md',
'settings.persona.builder.modeLabel': 'Modo do editor de persona',
'settings.persona.builder.modeGuided': 'Guiado',
'settings.persona.builder.modeAdvanced': 'Avançado',
'settings.persona.builder.intro':
'Preencha alguns campos e nós os escrevemos na sua persona. Não é preciso markdown.',
'settings.persona.builder.personalityLabel': 'Personalidade',
'settings.persona.builder.personalityPlaceholder':
'ex.: Acolhedor, curioso e direto. Honesto quanto à incerteza.',
'settings.persona.builder.voiceLabel': 'Estilo de comunicação',
'settings.persona.builder.voicePlaceholder':
'ex.: Comece pela resposta, seja breve e acompanhe o meu tom.',
'settings.persona.builder.aboutLabel': 'Sobre você',
'settings.persona.builder.aboutPlaceholder':
'ex.: Tenho um pequeno estúdio de design e prefiro linguagem simples.',
'settings.persona.builder.preservedNote':
'Quaisquer outras seções que você escreveu à mão são mantidas — mude para Avançado para ver a persona completa.',
'settings.persona.builder.securityNote': 'Quer escolher o que o assistente pode fazer?',
'settings.persona.builder.securityLink': 'Abrir Acesso do agente',
'settings.persona.templates.heading': 'Começar a partir de um modelo',
'settings.persona.templates.desc':
'Escolha um ponto de partida: preenche Personalidade e Estilo de comunicação. Depois você pode editar tudo.',
'settings.persona.templates.doctor.label': 'Assistente clínico',
'settings.persona.templates.doctor.desc': 'Cuidadoso, cita fontes, sinaliza incerteza',
'settings.persona.templates.researcher.label': 'Assistente de pesquisa',
'settings.persona.templates.researcher.desc': 'Rigoroso, estruturado, baseado em evidências',
'settings.persona.templates.executive.label': 'Assistente executivo',
'settings.persona.templates.executive.desc': 'Conciso, decidido, orientado à ação',
'settings.persona.templates.teacher.label': 'Professor',
'settings.persona.templates.teacher.desc': 'Paciente, explica passo a passo',
'settings.persona.templates.student.label': 'Parceiro de estudos',
'settings.persona.templates.student.desc': 'Encorajador, faz perguntas, linguagem simples',
'settings.persona.templates.family.label': 'Assistente da família',
'settings.persona.templates.family.desc': 'Acolhedor, amigável, seguro para todas as idades',
'settings.persona.appearanceHeading': 'Avatar e Voz',
'settings.persona.appearanceDesc':
'A cor do mascote, o avatar personalizado GIF e a voz de resposta são configurados nas configurações do mascote.',
+34
View File
@@ -5093,6 +5093,40 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': 'Не удалось загрузить SOUL.md.',
'settings.persona.soul.saveError': 'Не удалось сохранить SOUL.md.',
'settings.persona.soul.resetError': 'Не удалось сбросить SOUL.md.',
'settings.persona.builder.modeLabel': 'Режим редактора персоны',
'settings.persona.builder.modeGuided': 'С подсказками',
'settings.persona.builder.modeAdvanced': 'Расширенный',
'settings.persona.builder.intro':
'Заполните несколько полей, и мы впишем их в вашу персону. Markdown не нужен.',
'settings.persona.builder.personalityLabel': 'Характер',
'settings.persona.builder.personalityPlaceholder':
'напр. Тёплый, любознательный и прямой. Честен в неопределённости.',
'settings.persona.builder.voiceLabel': 'Стиль общения',
'settings.persona.builder.voicePlaceholder': 'напр. Сначала ответ, кратко и в моём тоне.',
'settings.persona.builder.aboutLabel': 'О вас',
'settings.persona.builder.aboutPlaceholder':
'напр. У меня небольшая дизайн-студия, предпочитаю простой язык.',
'settings.persona.builder.preservedNote':
'Все другие разделы, написанные вручную, сохраняются — переключитесь на «Расширенный», чтобы увидеть персону целиком.',
'settings.persona.builder.securityNote': 'Хотите выбрать, что разрешено ассистенту?',
'settings.persona.builder.securityLink': 'Открыть доступ агента',
'settings.persona.templates.heading': 'Начать с шаблона',
'settings.persona.templates.desc':
'Выберите отправную точку — она заполнит «Характер» и «Стиль общения». Потом всё можно изменить.',
'settings.persona.templates.doctor.label': 'Клинический ассистент',
'settings.persona.templates.doctor.desc':
'Внимателен, ссылается на источники, отмечает неопределённость',
'settings.persona.templates.researcher.label': 'Научный ассистент',
'settings.persona.templates.researcher.desc':
'Строгий, структурированный, опирается на доказательства',
'settings.persona.templates.executive.label': 'Ассистент руководителя',
'settings.persona.templates.executive.desc': 'Краткий, решительный, ориентирован на действие',
'settings.persona.templates.teacher.label': 'Учитель',
'settings.persona.templates.teacher.desc': 'Терпеливый, объясняет шаг за шагом',
'settings.persona.templates.student.label': 'Помощник в учёбе',
'settings.persona.templates.student.desc': 'Поддерживающий, задаёт вопросы, простой язык',
'settings.persona.templates.family.label': 'Семейный ассистент',
'settings.persona.templates.family.desc': 'Тёплый, дружелюбный, подходит для всех возрастов',
'settings.persona.appearanceHeading': 'Аватар и голос',
'settings.persona.appearanceDesc':
'Цвет талисмана, пользовательский аватар GIF и голос ответа настраиваются в настройках талисмана.',
+29
View File
@@ -4785,6 +4785,35 @@ const messages: TranslationMap = {
'settings.persona.soul.loadError': '无法加载 SOUL.md',
'settings.persona.soul.saveError': '无法保存 SOUL.md',
'settings.persona.soul.resetError': '无法重置 SOUL.md',
'settings.persona.builder.modeLabel': '角色编辑器模式',
'settings.persona.builder.modeGuided': '引导式',
'settings.persona.builder.modeAdvanced': '高级',
'settings.persona.builder.intro': '填写几个字段,我们会替你写入角色设定。无需 Markdown。',
'settings.persona.builder.personalityLabel': '性格',
'settings.persona.builder.personalityPlaceholder': '例如:温暖、好奇、直接。对不确定坦诚。',
'settings.persona.builder.voiceLabel': '沟通风格',
'settings.persona.builder.voicePlaceholder': '例如:先给答案,简洁,并贴合我的语气。',
'settings.persona.builder.aboutLabel': '关于你',
'settings.persona.builder.aboutPlaceholder': '例如:我经营一家小型设计工作室,喜欢通俗的表达。',
'settings.persona.builder.preservedNote':
'你手写的其他部分都会保留——切换到「高级」可查看完整角色设定。',
'settings.persona.builder.securityNote': '想选择助手可以做什么?',
'settings.persona.builder.securityLink': '打开代理访问权限',
'settings.persona.templates.heading': '从模板开始',
'settings.persona.templates.desc':
'选择一个起点——它会填写「性格」和「沟通风格」。之后你可以随意修改。',
'settings.persona.templates.doctor.label': '临床助手',
'settings.persona.templates.doctor.desc': '严谨、引用来源、标注不确定',
'settings.persona.templates.researcher.label': '研究助手',
'settings.persona.templates.researcher.desc': '严谨、有条理、以证据为先',
'settings.persona.templates.executive.label': '高管助手',
'settings.persona.templates.executive.desc': '简洁、果断、注重行动',
'settings.persona.templates.teacher.label': '老师',
'settings.persona.templates.teacher.desc': '有耐心、循序渐进地讲解',
'settings.persona.templates.student.label': '学习搭子',
'settings.persona.templates.student.desc': '鼓励式、出小测、通俗易懂',
'settings.persona.templates.family.label': '家庭助手',
'settings.persona.templates.family.desc': '温暖、友好、老少皆宜',
'settings.persona.appearanceHeading': '头像和声音',
'settings.persona.appearanceDesc': '吉祥物颜色、自定义 GIF 头像和回复声音在吉祥物设置中配置。',
'settings.persona.openMascotSettings': '打开吉祥物设置',