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

use serde::Deserialize;

const BASE: &str = include_str!("../../prompts/base.md");
const SUMMARY: &str = include_str!("../../prompts/summary.md");
const TASKS: &str = include_str!("../../prompts/tasks.md");
const REVIEW: &str = include_str!("../../prompts/review.md");

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "kind", content = "text", rename_all = "snake_case")]
pub enum Command {
    Summary,
    Tasks,
    Review,
    /// Свой запрос: «напиши письмо клиенту по итогам».
    Custom(String),
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
    #[error("Напишите, что сделать")]
    EmptyRequest,
    #[error("Сначала расшифруйте запись")]
    EmptyNote,
}

/// Что отправить модели и под каким заголовком вставить ответ.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prepared {
    pub heading: String,
    pub system: String,
    pub user: String,
}

/// Заголовок раздела, в который ляжет ответ команды.
pub fn heading(command: &Command) -> Result<String, Error> {
    Ok(match command {
        Command::Summary => "Конспект".to_string(),
        Command::Tasks => "Задачи".to_string(),
        Command::Review => "Ревью".to_string(),
        Command::Custom(request) => {
            let request = request.trim();
            if request.is_empty() {
                return Err(Error::EmptyRequest);
            }
            heading_for(request)
        }
    })
}

pub fn prepare(command: &Command, note: &str) -> Result<Prepared, Error> {
    let heading = heading(command)?;
    if note.trim().is_empty() {
        return Err(Error::EmptyNote);
    }
    let task = match command {
        Command::Summary => SUMMARY.trim().to_string(),
        Command::Tasks => TASKS.trim().to_string(),
        Command::Review => REVIEW.trim().to_string(),
        Command::Custom(request) => format!("Просьба человека: {}", request.trim()),
    };
    Ok(Prepared {
        heading,
        system: format!("{}\n\n## Что сделать сейчас\n\n{task}", BASE.trim()),
        user: format!("<заметка>\n{}\n</заметка>", note.trim()),
    })
}

/// Заголовок для своего запроса: сам запрос, укороченный до строки.
fn heading_for(request: &str) -> String {
    let line = request.lines().next().unwrap_or(request).trim();
    let mut chars = line.chars();
    let short: String = chars.by_ref().take(60).collect();
    let mut h = if chars.next().is_some() {
        format!("{}…", short.trim_end())
    } else {
        short
    };
    if let Some(first) = h.chars().next() {
        h = first.to_uppercase().collect::<String>() + &h[first.len_utf8()..];
    }
    h
}

/// Блок для вставки в заметку. Заголовки второго уровня в ответе модели
/// опускаются на ступень, чтобы не спорить с заголовком команды.
pub fn to_block(heading: &str, answer: &str) -> String {
    let body: Vec<String> = answer
        .trim()
        .lines()
        .map(|l| {
            if l.starts_with("## ") || l.starts_with("# ") {
                format!("### {}", l.trim_start_matches('#').trim())
            } else {
                l.to_string()
            }
        })
        .collect();
    format!("## {heading}\n\n{}\n", body.join("\n"))
}

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

    #[test]
    fn ready_commands_have_headings_and_task_text() {
        let p = prepare(&Command::Tasks, "## Расшифровка\n\nтекст").unwrap();
        assert_eq!(p.heading, "Задачи");
        assert!(p
            .system
            .starts_with("Ты помогаешь человеку получить пользу из записи встречи"));
        assert!(p.system.contains("- [ ] Что сделать"));
        assert_eq!(p.user, "<заметка>\n## Расшифровка\n\nтекст\n</заметка>");
        assert_eq!(prepare(&Command::Summary, "x").unwrap().heading, "Конспект");
        assert_eq!(prepare(&Command::Review, "x").unwrap().heading, "Ревью");
    }

    #[test]
    fn custom_request_becomes_heading_and_task() {
        let p = prepare(&Command::Custom("  напиши письмо клиенту по итогам ".into()), "x").unwrap();
        assert_eq!(p.heading, "Напиши письмо клиенту по итогам");
        assert!(p
            .system
            .ends_with("Просьба человека: напиши письмо клиенту по итогам"));
        let long = "а".repeat(100);
        assert_eq!(heading_for(&long).chars().count(), 61, "60 знаков и многоточие");
    }

    #[test]
    fn empty_input_is_refused() {
        assert_eq!(
            prepare(&Command::Custom("  ".into()), "x"),
            Err(Error::EmptyRequest)
        );
        assert_eq!(prepare(&Command::Summary, " \n "), Err(Error::EmptyNote));
    }

    #[test]
    fn answer_headings_are_demoted() {
        assert_eq!(
            to_block("Конспект", "# Конспект\n\nТекст.\n## Решения\n- Одно\n### Детали"),
            "## Конспект\n\n### Конспект\n\nТекст.\n### Решения\n- Одно\n### Детали\n"
        );
    }

    #[test]
    fn command_comes_from_the_interface_as_json() {
        let c: Command = serde_json::from_str(r#"{"kind":"custom","text":"выпиши цифры"}"#).unwrap();
        assert_eq!(c, Command::Custom("выпиши цифры".into()));
        let c: Command = serde_json::from_str(r#"{"kind":"summary"}"#).unwrap();
        assert_eq!(c, Command::Summary);
    }
}