feat(desktop): send meeting transcript segments and preset prompt edits to the phone

The phone draws a meeting from its transcript segments before the edited
transcript, so desktop edits, auto-polish and diarization never showed there.
Every desktop transcript change now rebuilds the meeting's segments from its
[MM:SS] [speaker] lines and trims the rest; the line parser moves to
@d3ro/core/meeting-transcript and the meeting view uses it too.

Prompt edits of the four desktop presets that exist on the phone update the
server preset row (a reset restores its default; {{targetLanguage}} is sent
as English, the only target on both sides), and edits made on another desktop
come back. The free-prompt preset has no phone counterpart and stays local.
This commit is contained in:
Yun Chan 2026-09-27 16:24:25 +09:00
parent 08c6504589
commit 2aac10fc5d
14 changed files with 552 additions and 28 deletions

View file

@ -156,6 +156,36 @@ export class FakeSyncRemote implements SyncRemote {
for (let i = rows.length - 1; i >= 0; i--) if (rows[i][parentColumn] === parentId) rows.splice(i, 1)
}
async deleteChildrenFrom(
table: string,
parentColumn: string,
parentId: string,
indexColumn: string,
fromIndex: number
): Promise<void> {
this.guard()
const rows = this.rows(table)
for (let i = rows.length - 1; i >= 0; i--) {
if (rows[i][parentColumn] === parentId && Number(rows[i][indexColumn]) >= fromIndex) rows.splice(i, 1)
}
}
/** 서버 프리셋 기본 프롬프트 (마이그레이션 0036 과 같은 문구) */
static readonly BUILTIN_DEFAULTS: Record<string, string> = {
translate_en: 'Translate the following text into natural English. Return only the translation.',
summarize: 'Summarize the following text in no more than three concise lines.',
formal: 'Rewrite the following text in a formal business style while preserving its meaning. Return only the rewritten text.',
explain_code: 'Explain the following code in Korean, including its responsibilities and important caveats.',
}
private ensureBuiltins(): void {
for (const [key, prompt] of Object.entries(FakeSyncRemote.BUILTIN_DEFAULTS)) {
if (!this.rows('custom_instructions').some((r) => r.builtin_key === key)) {
this.mobileInsert('custom_instructions', { id: crypto.randomUUID(), builtin_key: key, name: key, prompt, icon: 'x', sort_order: 10 })
}
}
}
async invokeFunction(name: string, body: Record<string, unknown>): Promise<unknown> {
this.guard()
this.invoked.push(`${name}:${String(body.document_id ?? '')}`)
@ -298,6 +328,26 @@ export class FakeSyncRemote implements SyncRemote {
else this.mobileInsert('user_settings', { active_instruction_id: instructionId, revision: 1 })
return row
}
if (name === 'sync_set_builtin_instruction_prompt_v1') {
this.ensureBuiltins()
const key = String(params.p_builtin_key)
const fallback = FakeSyncRemote.BUILTIN_DEFAULTS[key]
if (!fallback) throw new SyncRemoteError('invalid_builtin_key', '22023', false)
const next = typeof params.p_prompt === 'string' && params.p_prompt.trim() ? params.p_prompt.trim() : fallback
const row = this.rows('custom_instructions').find((r) => r.builtin_key === key)!
if (row.prompt !== next) Object.assign(row, { prompt: next, revision: Number(row.revision) + 1, updated_at: this.now() })
return row
}
if (name === 'sync_list_builtin_instructions_v1') {
return this.rows('custom_instructions')
.filter((r) => typeof r.builtin_key === 'string')
.map((r) => ({
builtin_key: r.builtin_key,
prompt: r.prompt,
is_default: r.prompt === FakeSyncRemote.BUILTIN_DEFAULTS[String(r.builtin_key)],
updated_at: r.updated_at,
}))
}
if (name === 'sync_delete_user_template_v1') {
const id = String(params.p_id)
if (!this.find('user_templates', id)) return false