diff --git a/app/src/components/settings/panels/PersonaPanel.test.tsx b/app/src/components/settings/panels/PersonaPanel.test.tsx index 3f8661142d..d036d33ba4 100644 --- a/app/src/components/settings/panels/PersonaPanel.test.tsx +++ b/app/src/components/settings/panels/PersonaPanel.test.tsx @@ -40,6 +40,13 @@ const soulFile = (overrides: Record = {}) => ({ ...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(); + 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(); + 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(); + 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(screen.getByTestId('persona-soul-editor')).toHaveValue('You are helpful.'); + 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(); + 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'); }); - expect(readPersonaFileMock).toHaveBeenCalledWith('SOUL.md'); }); it('persists the display name to the store on save', async () => { const { store } = renderWithProviders(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); + await awaitLoaded(); + fireEvent.click(screen.getByTestId('persona-guided-agent-access')); + expect(mockNavigateToSettings).toHaveBeenCalledWith('agent-access'); + }); }); diff --git a/app/src/components/settings/panels/PersonaPanel.tsx b/app/src/components/settings/panels/PersonaPanel.tsx index c406bb0bab..4843aae7fa 100644 --- a/app/src/components/settings/panels/PersonaPanel.tsx +++ b/app/src/components/settings/panels/PersonaPanel.tsx @@ -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(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('guided'); useEffect(() => { let cancelled = false; @@ -195,17 +201,44 @@ const PersonaPanel = ({ embedded = false }: PersonaPanelProps) => { ) : ( <> -
- setSoulDraft(e.target.value)} - /> +
+ +
+ {soulMode === 'guided' ? ( + + ) : ( +
+ setSoulDraft(e.target.value)} + /> +
+ )}
+

+
+ ); +}; + +export default PersonaGuidedFields; diff --git a/app/src/components/settings/panels/persona/PersonaTemplatePicker.tsx b/app/src/components/settings/panels/persona/PersonaTemplatePicker.tsx new file mode 100644 index 0000000000..ec79180737 --- /dev/null +++ b/app/src/components/settings/panels/persona/PersonaTemplatePicker.tsx @@ -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 ( +
+
+

+ {t('settings.persona.templates.heading')} +

+

+ {t('settings.persona.templates.desc')} +

+
+
+ {PERSONA_TEMPLATES.map(template => ( + + ))} +
+
+ ); +}; + +export default PersonaTemplatePicker; diff --git a/app/src/components/settings/panels/persona/personaSections.test.ts b/app/src/components/settings/panels/persona/personaSections.test.ts new file mode 100644 index 0000000000..fa6d189576 --- /dev/null +++ b/app/src/components/settings/panels/persona/personaSections.test.ts @@ -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); + }); +}); diff --git a/app/src/components/settings/panels/persona/personaSections.ts b/app/src/components/settings/panels/persona/personaSections.ts new file mode 100644 index 0000000000..dae3c7fa6c --- /dev/null +++ b/app/src/components/settings/panels/persona/personaSections.ts @@ -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; + +const HEADING_FOR: Record = { + 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 `## ` 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 `## ` + * 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; +} diff --git a/app/src/components/settings/panels/persona/personaTemplates.test.ts b/app/src/components/settings/panels/persona/personaTemplates.test.ts new file mode 100644 index 0000000000..6d50d45f98 --- /dev/null +++ b/app/src/components/settings/panels/persona/personaTemplates.test.ts @@ -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); + }); +}); diff --git a/app/src/components/settings/panels/persona/personaTemplates.ts b/app/src/components/settings/panels/persona/personaTemplates.ts new file mode 100644 index 0000000000..c1b16e535d --- /dev/null +++ b/app/src/components/settings/panels/persona/personaTemplates.ts @@ -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; +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 22ca4d8c42..c5b9c4893c 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4867,6 +4867,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، وصوت الرد مصمم في أماكن مكوت.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 7265f80cab..0097be7361 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4967,6 +4967,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 বৈশিষ্ট্যের মধ্য থেকে ভয়েস কনফিগার করা হয়েছে।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 7552c1c1ca..d3e7a8f889 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -5096,6 +5096,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.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 82914a54af..a27beb5f6c 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -5607,6 +5607,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.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index cf8bfc7c79..641b869fc7 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -5061,6 +5061,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.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 4292d98952..b5b8de163d 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -5080,6 +5080,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 à l’incertitude.', + '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 l’assistant est autorisé à faire ?', + 'settings.persona.builder.securityLink': 'Ouvrir Accès de l’agent', + 'settings.persona.templates.heading': 'Partir d’un 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 l’incertitude', + '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.", diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 3243bd11bc..aa9c0de96a 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4971,6 +4971,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 सेटिंग्स में कॉन्फ़िगर किया गया है।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 95e225fbd0..c6cb7d4fcd 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4984,6 +4984,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.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 580753ee50..ab8e812a85 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -5052,6 +5052,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 sull’incertezza.', + '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 l’assistente?', + '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 l’incertezza', + '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 all’azione', + '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.", diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 74046271b0..0f8600a218 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4920,6 +4920,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 아바타, 응답 음성은 마스코트 설정에서 구성합니다.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index ee94ffa56b..dbcb3ad87d 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -5038,6 +5038,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.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 0b60a812b9..e36145982f 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -5054,6 +5054,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.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 8f4ac3700b..9dd82733e4 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -5013,6 +5013,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 и голос ответа настраиваются в настройках талисмана.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 53e158ebd8..d73b15814d 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4724,6 +4724,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': '打开吉祥物设置',