From 941d38a2fd6314dfc1492cac62e71f69c9615044 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Thu, 2 Jul 2026 17:13:35 +0530 Subject: [PATCH 1/3] feat(persona): guided persona builder over SOUL.md (#4253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a structured, non-technical persona editor that maps friendly fields (Personality, Communication style, About you) to named SOUL.md sections and splices them in place, keeping SOUL.md the runtime source of truth. Guided is the default; the raw markdown editor stays behind an Advanced toggle. Reuses the existing workspace_file_read/write/reset RPC — no core changes. Part of the phased #4253 work (PR1 of N). --- .../settings/panels/PersonaPanel.test.tsx | 78 ++++++++--- .../settings/panels/PersonaPanel.tsx | 53 +++++-- .../panels/persona/PersonaGuidedFields.tsx | 98 +++++++++++++ .../panels/persona/personaSections.test.ts | 91 ++++++++++++ .../panels/persona/personaSections.ts | 129 ++++++++++++++++++ app/src/lib/i18n/ar.ts | 13 ++ app/src/lib/i18n/bn.ts | 13 ++ app/src/lib/i18n/de.ts | 13 ++ app/src/lib/i18n/en.ts | 18 +++ app/src/lib/i18n/es.ts | 13 ++ app/src/lib/i18n/fr.ts | 13 ++ app/src/lib/i18n/hi.ts | 13 ++ app/src/lib/i18n/id.ts | 13 ++ app/src/lib/i18n/it.ts | 13 ++ app/src/lib/i18n/ko.ts | 13 ++ app/src/lib/i18n/pl.ts | 13 ++ app/src/lib/i18n/pt.ts | 13 ++ app/src/lib/i18n/ru.ts | 13 ++ app/src/lib/i18n/zh-CN.ts | 13 ++ 19 files changed, 609 insertions(+), 27 deletions(-) create mode 100644 app/src/components/settings/panels/persona/PersonaGuidedFields.tsx create mode 100644 app/src/components/settings/panels/persona/personaSections.test.ts create mode 100644 app/src/components/settings/panels/persona/personaSections.ts diff --git a/app/src/components/settings/panels/PersonaPanel.test.tsx b/app/src/components/settings/panels/PersonaPanel.test.tsx index 3f8661142d..d32b0c81a1 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(); @@ -47,22 +54,50 @@ describe('PersonaPanel', () => { writePersonaFileMock.mockImplementation((_name: string, contents: string) => Promise.resolve(soulFile({ contents, is_default: false })) ); - resetPersonaFileMock.mockResolvedValue( - soulFile({ contents: 'default soul', is_default: true }) - ); + resetPersonaFileMock.mockResolvedValue(soulFile({ contents: 'default soul', is_default: true })); + }); + + 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('loads SOUL.md contents into the editor on mount', async () => { + 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' + ); }); - 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 +113,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 +135,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 +150,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 +163,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 +177,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 +192,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/personaSections.test.ts b/app/src/components/settings/panels/persona/personaSections.test.ts new file mode 100644 index 0000000000..d6238bb703 --- /dev/null +++ b/app/src/components/settings/panels/persona/personaSections.test.ts @@ -0,0 +1,91 @@ +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/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 22ca4d8c42..a2f31478a3 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4867,6 +4867,19 @@ 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.appearanceHeading': 'صوت الأفاتار', 'settings.persona.appearanceDesc': 'لون الماسكوت، العرف Xqx0x avatar، وصوت الرد مصمم في أماكن مكوت.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 7265f80cab..3033547adf 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4967,6 +4967,19 @@ 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.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..aa1dd76b32 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -5096,6 +5096,19 @@ 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.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..11bf16309c 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -5607,6 +5607,24 @@ 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.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..6139a02217 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -5061,6 +5061,19 @@ 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.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..f0ca71d230 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -5080,6 +5080,19 @@ 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.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..592d99d3f2 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4971,6 +4971,19 @@ 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.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..6b2ad23e6d 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4984,6 +4984,19 @@ 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.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..d5a6d2c3cb 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -5052,6 +5052,19 @@ 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.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..19cbfe54f9 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4920,6 +4920,19 @@ 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.appearanceHeading': '아바타 및 음성', 'settings.persona.appearanceDesc': '마스코트 색상, 사용자 지정 GIF 아바타, 응답 음성은 마스코트 설정에서 구성합니다.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index ee94ffa56b..cc703b4e1c 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -5038,6 +5038,19 @@ 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.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..6d51ed9ba4 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -5054,6 +5054,19 @@ 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.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..22d47a55d0 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -5013,6 +5013,19 @@ 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.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..c679ee2e8c 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4724,6 +4724,19 @@ 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.appearanceHeading': '头像和声音', 'settings.persona.appearanceDesc': '吉祥物颜色、自定义 GIF 头像和回复声音在吉祥物设置中配置。', 'settings.persona.openMascotSettings': '打开吉祥物设置', From 7c750b4cc681ba431e7eab11c015449daf2b7884 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Thu, 2 Jul 2026 19:31:31 +0530 Subject: [PATCH 2/3] style: apply prettier to persona-builder files and i18n locales Prettier-only formatting of the persona builder components/tests and the guided-persona i18n key additions across all locale files. --- .../settings/panels/PersonaPanel.test.tsx | 4 +++- .../panels/persona/PersonaGuidedFields.tsx | 2 +- .../panels/persona/personaSections.test.ts | 6 +----- app/src/lib/i18n/ar.ts | 12 ++++++++---- app/src/lib/i18n/bn.ts | 15 ++++++++++----- app/src/lib/i18n/de.ts | 15 ++++++++++----- app/src/lib/i18n/es.ts | 15 ++++++++++----- app/src/lib/i18n/fr.ts | 18 ++++++++++++------ app/src/lib/i18n/hi.ts | 15 ++++++++++----- app/src/lib/i18n/id.ts | 15 ++++++++++----- app/src/lib/i18n/it.ts | 15 ++++++++++----- app/src/lib/i18n/ko.ts | 12 ++++++++---- app/src/lib/i18n/pl.ts | 15 ++++++++++----- app/src/lib/i18n/pt.ts | 15 ++++++++++----- app/src/lib/i18n/ru.ts | 12 ++++++++---- app/src/lib/i18n/zh-CN.ts | 3 ++- 16 files changed, 123 insertions(+), 66 deletions(-) diff --git a/app/src/components/settings/panels/PersonaPanel.test.tsx b/app/src/components/settings/panels/PersonaPanel.test.tsx index d32b0c81a1..f4dbc33352 100644 --- a/app/src/components/settings/panels/PersonaPanel.test.tsx +++ b/app/src/components/settings/panels/PersonaPanel.test.tsx @@ -54,7 +54,9 @@ describe('PersonaPanel', () => { writePersonaFileMock.mockImplementation((_name: string, contents: string) => Promise.resolve(soulFile({ contents, is_default: false })) ); - resetPersonaFileMock.mockResolvedValue(soulFile({ contents: 'default soul', is_default: true })); + resetPersonaFileMock.mockResolvedValue( + soulFile({ contents: 'default soul', is_default: true }) + ); }); it('defaults to the guided builder and hides raw markdown', async () => { diff --git a/app/src/components/settings/panels/persona/PersonaGuidedFields.tsx b/app/src/components/settings/panels/persona/PersonaGuidedFields.tsx index e520d11248..43076b59ca 100644 --- a/app/src/components/settings/panels/persona/PersonaGuidedFields.tsx +++ b/app/src/components/settings/panels/persona/PersonaGuidedFields.tsx @@ -1,6 +1,6 @@ import { useT } from '../../../../lib/i18n/I18nContext'; -import { useSettingsNavigation } from '../../hooks/useSettingsNavigation'; import { SettingsRow, SettingsTextArea } from '../../controls'; +import { useSettingsNavigation } from '../../hooks/useSettingsNavigation'; import { applyPersonaField, parsePersonaFields, type PersonaFieldKey } from './personaSections'; interface PersonaGuidedFieldsProps { diff --git a/app/src/components/settings/panels/persona/personaSections.test.ts b/app/src/components/settings/panels/persona/personaSections.test.ts index d6238bb703..fa6d189576 100644 --- a/app/src/components/settings/panels/persona/personaSections.test.ts +++ b/app/src/components/settings/panels/persona/personaSections.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - applyPersonaField, - applyPersonaFields, - parsePersonaFields, -} from './personaSections'; +import { applyPersonaField, applyPersonaFields, parsePersonaFields } from './personaSections'; const SOUL = `# OpenHuman diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index a2f31478a3..237c8ef6ff 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4870,14 +4870,18 @@ const messages: TranslationMap = { 'settings.persona.builder.modeLabel': 'وضع محرر الشخصية', 'settings.persona.builder.modeGuided': 'موجّه', 'settings.persona.builder.modeAdvanced': 'متقدّم', - 'settings.persona.builder.intro': 'املأ بضعة حقول وسنكتبها في شخصيتك نيابةً عنك. لا حاجة إلى ماركداون.', + 'settings.persona.builder.intro': + 'املأ بضعة حقول وسنكتبها في شخصيتك نيابةً عنك. لا حاجة إلى ماركداون.', 'settings.persona.builder.personalityLabel': 'الشخصية', - 'settings.persona.builder.personalityPlaceholder': 'مثال: ودود وفضولي ومباشر. صادق عند عدم اليقين.', + '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.aboutPlaceholder': + 'مثال: أدير استوديو تصميم صغيرًا وأفضّل اللغة البسيطة.', + 'settings.persona.builder.preservedNote': + 'تُحفظ أي أقسام أخرى كتبتها بنفسك — انتقل إلى «متقدّم» لرؤية الشخصية كاملة.', 'settings.persona.builder.securityNote': 'هل تريد تحديد ما يُسمح للمساعد بفعله؟', 'settings.persona.builder.securityLink': 'فتح وصول الوكيل', 'settings.persona.appearanceHeading': 'صوت الأفاتار', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 3033547adf..ec9e0df745 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4970,14 +4970,19 @@ const messages: TranslationMap = { 'settings.persona.builder.modeLabel': 'পারসোনা এডিটর মোড', 'settings.persona.builder.modeGuided': 'গাইডেড', 'settings.persona.builder.modeAdvanced': 'অ্যাডভান্সড', - 'settings.persona.builder.intro': 'কয়েকটি ঘর পূরণ করুন, আমরা সেগুলো আপনার পারসোনায় লিখে দেব। মার্কডাউন লাগবে না।', + 'settings.persona.builder.intro': + 'কয়েকটি ঘর পূরণ করুন, আমরা সেগুলো আপনার পারসোনায় লিখে দেব। মার্কডাউন লাগবে না।', 'settings.persona.builder.personalityLabel': 'ব্যক্তিত্ব', - 'settings.persona.builder.personalityPlaceholder': 'যেমন: উষ্ণ, কৌতূহলী ও স্পষ্টবাদী। অনিশ্চয়তায় সৎ।', + 'settings.persona.builder.personalityPlaceholder': + 'যেমন: উষ্ণ, কৌতূহলী ও স্পষ্টবাদী। অনিশ্চয়তায় সৎ।', 'settings.persona.builder.voiceLabel': 'যোগাযোগের ধরন', - 'settings.persona.builder.voicePlaceholder': 'যেমন: আগে উত্তর দিন, সংক্ষিপ্ত রাখুন এবং আমার সুরে মিলিয়ে নিন।', + 'settings.persona.builder.voicePlaceholder': + 'যেমন: আগে উত্তর দিন, সংক্ষিপ্ত রাখুন এবং আমার সুরে মিলিয়ে নিন।', 'settings.persona.builder.aboutLabel': 'আপনার সম্পর্কে', - 'settings.persona.builder.aboutPlaceholder': 'যেমন: আমি একটি ছোট ডিজাইন স্টুডিও চালাই এবং সহজ ভাষা পছন্দ করি।', - 'settings.persona.builder.preservedNote': 'আপনার নিজের হাতে লেখা অন্যান্য অংশ সংরক্ষিত থাকে — সম্পূর্ণ পারসোনা দেখতে অ্যাডভান্সড-এ যান।', + 'settings.persona.builder.aboutPlaceholder': + 'যেমন: আমি একটি ছোট ডিজাইন স্টুডিও চালাই এবং সহজ ভাষা পছন্দ করি।', + 'settings.persona.builder.preservedNote': + 'আপনার নিজের হাতে লেখা অন্যান্য অংশ সংরক্ষিত থাকে — সম্পূর্ণ পারসোনা দেখতে অ্যাডভান্সড-এ যান।', 'settings.persona.builder.securityNote': 'সহকারী কী করতে পারবে তা ঠিক করতে চান?', 'settings.persona.builder.securityLink': 'এজেন্ট অ্যাক্সেস খুলুন', 'settings.persona.appearanceHeading': 'অবতার & ভয়েস', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index aa1dd76b32..333074b53d 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -5099,14 +5099,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar und Stimme', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 6139a02217..4265877250 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -5064,14 +5064,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar y Voz', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index f0ca71d230..404de50aa2 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -5083,15 +5083,21 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar et Voix', 'settings.persona.appearanceDesc': diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 592d99d3f2..3c15779aed 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4974,14 +4974,19 @@ const messages: TranslationMap = { 'settings.persona.builder.modeLabel': 'पर्सोना एडिटर मोड', 'settings.persona.builder.modeGuided': 'निर्देशित', 'settings.persona.builder.modeAdvanced': 'उन्नत', - 'settings.persona.builder.intro': 'कुछ फ़ील्ड भरें और हम उन्हें आपके पर्सोना में लिख देंगे। मार्कडाउन की ज़रूरत नहीं।', + 'settings.persona.builder.intro': + 'कुछ फ़ील्ड भरें और हम उन्हें आपके पर्सोना में लिख देंगे। मार्कडाउन की ज़रूरत नहीं।', 'settings.persona.builder.personalityLabel': 'व्यक्तित्व', - 'settings.persona.builder.personalityPlaceholder': 'जैसे: गर्मजोश, जिज्ञासु और सीधा। अनिश्चितता पर ईमानदार।', + 'settings.persona.builder.personalityPlaceholder': + 'जैसे: गर्मजोश, जिज्ञासु और सीधा। अनिश्चितता पर ईमानदार।', 'settings.persona.builder.voiceLabel': 'संवाद शैली', - 'settings.persona.builder.voicePlaceholder': 'जैसे: पहले उत्तर दें, संक्षिप्त रखें और मेरे लहजे से मेल खाएँ।', + 'settings.persona.builder.voicePlaceholder': + 'जैसे: पहले उत्तर दें, संक्षिप्त रखें और मेरे लहजे से मेल खाएँ।', 'settings.persona.builder.aboutLabel': 'आपके बारे में', - 'settings.persona.builder.aboutPlaceholder': 'जैसे: मैं एक छोटा डिज़ाइन स्टूडियो चलाता हूँ और सरल भाषा पसंद करता हूँ।', - 'settings.persona.builder.preservedNote': 'आपके हाथ से लिखे अन्य अनुभाग सुरक्षित रहते हैं — पूरा पर्सोना देखने के लिए उन्नत पर जाएँ।', + 'settings.persona.builder.aboutPlaceholder': + 'जैसे: मैं एक छोटा डिज़ाइन स्टूडियो चलाता हूँ और सरल भाषा पसंद करता हूँ।', + 'settings.persona.builder.preservedNote': + 'आपके हाथ से लिखे अन्य अनुभाग सुरक्षित रहते हैं — पूरा पर्सोना देखने के लिए उन्नत पर जाएँ।', 'settings.persona.builder.securityNote': 'तय करना चाहते हैं कि असिस्टेंट क्या कर सकता है?', 'settings.persona.builder.securityLink': 'एजेंट एक्सेस खोलें', 'settings.persona.appearanceHeading': 'अवतार और आवाज', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 6b2ad23e6d..c8c8f5294b 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4987,14 +4987,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar & Suara', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index d5a6d2c3cb..d0b0509693 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -5055,14 +5055,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar e Voce', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 19cbfe54f9..a439055669 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4923,14 +4923,18 @@ const messages: TranslationMap = { 'settings.persona.builder.modeLabel': '페르소나 편집기 모드', 'settings.persona.builder.modeGuided': '가이드', 'settings.persona.builder.modeAdvanced': '고급', - 'settings.persona.builder.intro': '몇 가지 항목만 입력하면 페르소나에 대신 작성해 드립니다. 마크다운은 필요 없습니다.', + 'settings.persona.builder.intro': + '몇 가지 항목만 입력하면 페르소나에 대신 작성해 드립니다. 마크다운은 필요 없습니다.', 'settings.persona.builder.personalityLabel': '성격', - 'settings.persona.builder.personalityPlaceholder': '예: 따뜻하고 호기심 많고 직설적. 불확실할 땐 솔직하게.', + '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.aboutPlaceholder': + '예: 작은 디자인 스튜디오를 운영하며 쉬운 표현을 선호합니다.', + 'settings.persona.builder.preservedNote': + '직접 작성한 다른 섹션은 그대로 유지됩니다 — 전체 페르소나를 보려면 고급으로 전환하세요.', 'settings.persona.builder.securityNote': '어시스턴트가 할 수 있는 일을 정하시겠어요?', 'settings.persona.builder.securityLink': '에이전트 액세스 열기', 'settings.persona.appearanceHeading': '아바타 및 음성', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index cc703b4e1c..2450ee1997 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -5041,14 +5041,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Awatar i głos', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 6d51ed9ba4..ceabfff91a 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -5057,14 +5057,19 @@ const messages: TranslationMap = { '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.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.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.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.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.appearanceHeading': 'Avatar e Voz', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 22d47a55d0..598fcfafdb 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -5016,14 +5016,18 @@ const messages: TranslationMap = { 'settings.persona.builder.modeLabel': 'Режим редактора персоны', 'settings.persona.builder.modeGuided': 'С подсказками', 'settings.persona.builder.modeAdvanced': 'Расширенный', - 'settings.persona.builder.intro': 'Заполните несколько полей, и мы впишем их в вашу персону. Markdown не нужен.', + 'settings.persona.builder.intro': + 'Заполните несколько полей, и мы впишем их в вашу персону. Markdown не нужен.', 'settings.persona.builder.personalityLabel': 'Характер', - 'settings.persona.builder.personalityPlaceholder': 'напр. Тёплый, любознательный и прямой. Честен в неопределённости.', + '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.aboutPlaceholder': + 'напр. У меня небольшая дизайн-студия, предпочитаю простой язык.', + 'settings.persona.builder.preservedNote': + 'Все другие разделы, написанные вручную, сохраняются — переключитесь на «Расширенный», чтобы увидеть персону целиком.', 'settings.persona.builder.securityNote': 'Хотите выбрать, что разрешено ассистенту?', 'settings.persona.builder.securityLink': 'Открыть доступ агента', 'settings.persona.appearanceHeading': 'Аватар и голос', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index c679ee2e8c..39cdad3bf3 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4734,7 +4734,8 @@ const messages: TranslationMap = { 'settings.persona.builder.voicePlaceholder': '例如:先给答案,简洁,并贴合我的语气。', 'settings.persona.builder.aboutLabel': '关于你', 'settings.persona.builder.aboutPlaceholder': '例如:我经营一家小型设计工作室,喜欢通俗的表达。', - 'settings.persona.builder.preservedNote': '你手写的其他部分都会保留——切换到「高级」可查看完整角色设定。', + 'settings.persona.builder.preservedNote': + '你手写的其他部分都会保留——切换到「高级」可查看完整角色设定。', 'settings.persona.builder.securityNote': '想选择助手可以做什么?', 'settings.persona.builder.securityLink': '打开代理访问权限', 'settings.persona.appearanceHeading': '头像和声音', From 1a3430d7ede0c1b9e720721776c64d1dddc2d436 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 3 Jul 2026 22:02:16 +0530 Subject: [PATCH 3/3] fix(persona): preserve live newlines + stop whitespace growth in applyPersonaField MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit + Codex review on #4253: - Trimming the value on every keystroke stripped a trailing space/newline the user just typed (guided textareas are multiline lists). Trim only for the no-op comparison; write the raw value back into the document and defer trimming to save/serialize. - Lead/trail newline capture overlapped when the section body was pure whitespace (right after a clear): `^\n*` and `\n*$` both matched the whole string, so a clear→refill cycle doubled the surrounding blank lines and silently grew the document, breaking the file's lossless/idempotent guarantee. Split a pure-newline body canonically (one leading blank, remainder trailing) so the cycle converges to the same document a direct apply produces. Regression test: repeated clear→refill of a section is idempotent (twice === once) and preserves the byte-exact round-trip. 45 persona tests pass; typecheck clean. --- .../panels/persona/personaSections.test.ts | 13 ++++++++++ .../panels/persona/personaSections.ts | 25 +++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/src/components/settings/panels/persona/personaSections.test.ts b/app/src/components/settings/panels/persona/personaSections.test.ts index fa6d189576..20f9b7ec12 100644 --- a/app/src/components/settings/panels/persona/personaSections.test.ts +++ b/app/src/components/settings/panels/persona/personaSections.test.ts @@ -70,6 +70,19 @@ describe('applyPersonaField', () => { expect(next).toContain('## Voice'); expect(next).toContain('## When things go wrong'); }); + + it('does not grow surrounding whitespace across repeated clear → refill cycles', () => { + // After a clear the section body is pure newlines; refilling must not + // re-capture them as both lead *and* trail (which doubled the blank lines on + // every cycle, silently growing the document — the file's lossless/idempotent + // guarantee). One cycle and two cycles must produce the identical document. + const cycle = (doc: string) => + applyPersonaField(applyPersonaField(doc, 'voice', ''), 'voice', 'Be terse.'); + const once = cycle(SOUL); + const twice = cycle(once); + expect(twice).toBe(once); + expect(parsePersonaFields(once).voice).toBe('Be terse.'); + }); }); describe('applyPersonaFields round-trip', () => { diff --git a/app/src/components/settings/panels/persona/personaSections.ts b/app/src/components/settings/panels/persona/personaSections.ts index dae3c7fa6c..e32108f58c 100644 --- a/app/src/components/settings/panels/persona/personaSections.ts +++ b/app/src/components/settings/panels/persona/personaSections.ts @@ -101,22 +101,33 @@ export function parsePersonaFields(soul: string): PersonaFields { */ export function applyPersonaField(soul: string, key: PersonaFieldKey, value: string): string { const heading = HEADING_FOR[key]; - const nextBody = value.trim(); + // Normalize only for the no-op short-circuit; the value written back into the + // document is the raw `value`, so a trailing space/newline typed live in the + // guided textarea survives the round-trip (trimming is deferred to save). + const normalized = value.trim(); - if (readSection(soul, heading) === nextBody) return soul; + if (readSection(soul, heading) === normalized) 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}`; + // Capture the blank-line seams so they can be restored around the new body. + // When `raw` is all newlines (e.g. right after a clear splices lead+trail + // together), `^\n*` and `\n*$` both match the entire string and overlap, so a + // naive capture doubles the surrounding blank lines on every clear→refill + // cycle. Split such a body canonically — one leading blank, the remainder + // trailing — so the cycle converges to the same document a direct apply + // produces instead of growing. + const allNewlines = raw.length > 0 && !/[^\n]/.test(raw); + const lead = allNewlines ? raw.slice(0, 1) : (raw.match(/^\n*/)?.[0] ?? ''); + const trail = allNewlines ? raw.slice(lead.length) : (raw.match(/\n*$/)?.[0] ?? ''); + const spliced = value ? `${lead}${value}${trail || '\n'}` : `${lead}${trail}`; return soul.slice(0, span.bodyStart) + spliced + soul.slice(span.bodyEnd); } - if (!nextBody) return soul; + if (!value) return soul; const base = soul.replace(/\n*$/, '\n'); - return `${base}\n## ${heading}\n\n${nextBody}\n`; + return `${base}\n## ${heading}\n\n${value}\n`; } /** Apply every managed field at once (used for save-all / tests). */