skillok meeting
955f445c6196281d40574259b51317a618aa5376
Перейти к файлу
Код
meeting src-taurisrcnotes.rs
anc Простой путь: записать → расшифровать → получить результат - Главная: «Записать» (или аудиофайл перетаскиванием) и записи со статусом. Боковая панель и «Новая заметка» убраны. - Экран записи: шаги «Аудио → Расшифровка → Результат», на экране только текущий шаг и одна главная кнопка. - Результаты вкладками: «Расшифровка», «Конспект», «Задачи», «Ревью», «Свой запрос»; копировать, как текст, править, сделать заново, удалить. Повтор команды заменяет раздел, а не дописывает второй. - Задача #9: без ключа кнопки ведут к подключению ИИ; у расшифровки и ответа ИИ — прогресс и «Отменить» (CancellationToken в ядре); идущая запись видна меткой в шапке на любом экране. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> e75bd24 30 сентября 2026 г.
notes.rs
//! Разделы заметки. Заметка — Markdown; раздел — всё от заголовка второго
//! уровня (`## Задачи`) до следующего такого же. Интерфейс показывает разделы
//! вкладками, поэтому результат команды не дописывается вторым разделом с тем
//! же заголовком, а заменяет прежний.
//!
//! Правило разбиения то же, что в `src/markdown.ts`: `## ` в начале строки вне
//! блока кода ``` / ~~~.

/// Заголовки разделов по порядку.
pub fn headings(note: &str) -> Vec<String> {
    split(note).into_iter().filter_map(|(h, _)| h).collect()
}

/// Кладёт `block` (он начинается с `## heading`) на место раздела с тем же
/// заголовком или, если такого нет, в конец. Прочие разделы не трогает.
pub fn upsert(note: &str, heading: &str, block: &str) -> String {
    let block = block.trim().to_string();
    let mut parts = split(note);
    match parts.iter_mut().find(|(h, _)| h.as_deref() == Some(heading)) {
        Some(part) => part.1 = block,
        None => parts.push((Some(heading.to_string()), block)),
    }
    join(parts)
}

/// Убирает раздел с таким заголовком, если он есть.
pub fn remove(note: &str, heading: &str) -> String {
    join(
        split(note)
            .into_iter()
            .filter(|(h, _)| h.as_deref() != Some(heading))
            .collect(),
    )
}

/// Разделы: заголовок (у текста до первого заголовка — нет) и исходник целиком.
fn split(note: &str) -> Vec<(Option<String>, String)> {
    let mut parts: Vec<(Option<String>, String)> = Vec::new();
    let mut current: (Option<String>, Vec<&str>) = (None, Vec::new());
    let mut fence = false;
    for line in note.lines() {
        if line.starts_with("```") || line.starts_with("~~~") {
            fence = !fence;
        }
        if !fence {
            if let Some(h) = line.strip_prefix("## ") {
                let done = std::mem::replace(&mut current, (Some(h.trim().to_string()), Vec::new()));
                push(&mut parts, done);
            }
        }
        current.1.push(line);
    }
    push(&mut parts, current);
    parts
}

fn push(parts: &mut Vec<(Option<String>, String)>, (heading, lines): (Option<String>, Vec<&str>)) {
    let text = lines.join("\n").trim().to_string();
    if !text.is_empty() {
        parts.push((heading, text));
    }
}

fn join(parts: Vec<(Option<String>, String)>) -> String {
    let body: Vec<String> = parts
        .into_iter()
        .map(|(_, t)| t)
        .filter(|t| !t.is_empty())
        .collect();
    if body.is_empty() {
        String::new()
    } else {
        body.join("\n\n") + "\n"
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const NOTE: &str = "## Расшифровка\n\n**00:00:01 Я:** Привет.\n\n## Задачи\n\n- [ ] Отчёт\n";

    #[test]
    fn new_section_goes_to_the_end() {
        assert_eq!(
            upsert(NOTE, "Конспект", "## Конспект\n\nКоротко.\n"),
            "## Расшифровка\n\n**00:00:01 Я:** Привет.\n\n## Задачи\n\n- [ ] Отчёт\n\n## Конспект\n\nКоротко.\n"
        );
        assert_eq!(
            upsert("", "Расшифровка", "## Расшифровка\n\nтекст"),
            "## Расшифровка\n\nтекст\n"
        );
    }

    #[test]
    fn same_heading_is_replaced_in_place() {
        let out = upsert(
            NOTE,
            "Расшифровка",
            "## Расшифровка\n\n**00:00:01 Я:** Здравствуйте.",
        );
        assert_eq!(
            out,
            "## Расшифровка\n\n**00:00:01 Я:** Здравствуйте.\n\n## Задачи\n\n- [ ] Отчёт\n"
        );
        assert_eq!(headings(&out), vec!["Расшифровка", "Задачи"]);
    }

    #[test]
    fn text_before_first_heading_and_code_blocks_survive() {
        let note = "Мои пометки.\n\n## Письмо\n\n```\n## не заголовок\n```\n";
        assert_eq!(headings(note), vec!["Письмо"]);
        let out = upsert(note, "Задачи", "## Задачи\n\n- [ ] Позвонить");
        assert!(
            out.starts_with("Мои пометки.\n\n## Письмо\n\n```\n## не заголовок\n```\n\n## Задачи"),
            "{out}"
        );
    }

    #[test]
    fn remove_drops_only_that_section() {
        assert_eq!(
            remove(NOTE, "Задачи"),
            "## Расшифровка\n\n**00:00:01 Я:** Привет.\n"
        );
        assert_eq!(remove(NOTE, "Нет такого"), NOTE);
        assert_eq!(remove("## Задачи\n\n- [ ] x\n", "Задачи"), "");
    }
}