anc
Простой путь: записать → расшифровать → получить результат
- Главная: «Записать» (или аудиофайл перетаскиванием) и записи со статусом.
Боковая панель и «Новая заметка» убраны.
- Экран записи: шаги «Аудио → Расшифровка → Результат», на экране только
текущий шаг и одна главная кнопка.
- Результаты вкладками: «Расшифровка», «Конспект», «Задачи», «Ревью»,
«Свой запрос»; копировать, как текст, править, сделать заново, удалить.
Повтор команды заменяет раздел, а не дописывает второй.
- Задача #9: без ключа кнопки ведут к подключению ИИ; у расшифровки и
ответа ИИ — прогресс и «Отменить» (CancellationToken в ядре); идущая
запись видна меткой в шапке на любом экране.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e75bd24
30 сентября 2026 г.
markdown.ts
// Markdown заметки: отображение и деление на разделы.
//
// Заметка — единственный источник текста. В режиме просмотра она делится на
// разделы по заголовкам второго уровня (`## Расшифровка`, `## Задачи`), и у
// каждого раздела своя кнопка «Копировать» — копируется исходный Markdown
// раздела, а не HTML.
import { marked } from "marked";
import DOMPurify from "dompurify";
marked.setOptions({ gfm: true, breaks: false });
export function render(md: string): string {
return DOMPurify.sanitize(marked.parse(md, { async: false }));
}
export interface Section {
/** Заголовок раздела без `## `; у текста до первого заголовка — пусто. */
heading: string;
/** Исходный Markdown раздела вместе с заголовком. */
source: string;
}
export function sections(md: string): Section[] {
const out: Section[] = [];
let current: Section = { heading: "", source: "" };
let fence = false;
for (const line of md.split("\n")) {
if (/^(```|~~~)/.test(line)) fence = !fence;
const h = !fence && /^## (.+)$/.exec(line);
if (h) {
if (current.source.trim()) out.push(current);
current = { heading: h[1].trim(), source: "" };
}
current.source += line + "\n";
}
if (current.source.trim()) out.push(current);
for (const s of out) s.source = s.source.trim() + "\n";
return out;
}
/** Текст без разметки — для вставки туда, где Markdown не понимают. */
export function plain(md: string): string {
const div = document.createElement("div");
div.innerHTML = render(md);
return (div.innerText || div.textContent || "").replace(/\n{3,}/g, "\n\n").trim();
}
/**
* Переключает n-ю по счёту галочку списка задач (`- [ ]` ↔ `- [x]`) в
* исходнике. Порядок тот же, что у галочек в отображении: код в ``` не в счёт.
*/
export function toggleTask(md: string, n: number): string {
let fence = false;
let seen = 0;
return md
.split("\n")
.map((line) => {
if (/^(```|~~~)/.test(line)) fence = !fence;
if (fence) return line;
const m = /^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]/.exec(line);
if (!m) return line;
if (seen++ !== n) return line;
const mark = m[2] === " " ? "x" : " ";
return m[1] + `[${mark}]` + line.slice(m[0].length);
})
.join("\n");
}
/** Кладёт раздел на место прежнего с тем же заголовком или в конец — как
* `notes::upsert` в ядре. `source` начинается с `## заголовок`. */
export function upsertSection(md: string, heading: string, source: string): string {
const parts = sections(md);
const block = { heading, source: source.trim() + "\n" };
const i = parts.findIndex((s) => s.heading === heading);
if (i >= 0) parts[i] = block;
else parts.push(block);
return parts.map((s) => s.source.trim()).join("\n\n") + "\n";
}
/** Тело раздела без строки заголовка — для правки. */
export function sectionBody(source: string): string {
return source.replace(/^## .*\n?/, "").trim();
}