From 79f509951cbe9cd9f4145af897ccfb2c507ba141 Mon Sep 17 00:00:00 2001 From: "michael.borak" Date: Tue, 20 Jan 2026 17:15:14 +0100 Subject: [PATCH] Release 1.1: Pre-meeting models, Calendar improvements, Logs, Compact Recorder --- README.md | 1 + package.json | 4 +- src-tauri/Cargo.lock | 460 ++++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/src/audio_processor.rs | 19 +- src-tauri/src/email.rs | 96 ++++ src-tauri/src/lib.rs | 22 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 140 +++++- src/components/EmailPreviewModal.tsx | 280 ++++++++++++ src/components/EmailTemplateEditor.tsx | 216 +++++++++ src/components/MeetingsView.tsx | 246 ++++++---- src/components/Recorder.tsx | 64 ++- src/components/Settings.tsx | 600 +++++++++++++++++++------ src/components/Tabs.tsx | 17 +- src/components/TranscriptionView.tsx | 161 ++++++- 16 files changed, 2011 insertions(+), 321 deletions(-) create mode 100644 src-tauri/src/email.rs create mode 100644 src/components/EmailPreviewModal.tsx create mode 100644 src/components/EmailTemplateEditor.tsx diff --git a/README.md b/README.md index 54780cc..915e37e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ * **Upcoming Meetings**: View your daily schedule and join with **one click**. * **Meeting Details**: View full agenda and **invited attendee status** (Accepted/Declined). * **💾 Persistent History**: Automatically saves all transcripts and summaries to disk. Search and review past meetings anytime. +* **✉️ Email Summaries**: Send professional, formatted HTML summaries (with preview) directly to attendees via your own SMTP server. * **🧠 Powered by Infomaniak AI**: * **Precision Transcription**: Standard-compliant formatting with **second-by-second timestamps**. * **Smart Summaries**: Uses **Smart Templates** to automatically select the best format (Business Protocol vs. 1:1) based on meeting content. diff --git a/package.json b/package.json index cd31718..5fdcb95 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hearbit-ai", "private": true, - "version": "0.1.0", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite", @@ -33,4 +33,4 @@ "typescript": "~5.8.3", "vite": "^7.0.4" } -} +} \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 26cac17..aaabccf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,29 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -32,6 +55,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.10.0" @@ -54,6 +83,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -69,6 +115,21 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -274,6 +335,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -305,6 +378,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "brotli" version = "8.0.2" @@ -332,6 +428,40 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.0", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.24.0" @@ -463,6 +593,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -477,6 +613,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "combine" version = "4.6.7" @@ -879,6 +1025,22 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64 0.22.1", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "embed-resource" version = "3.0.6" @@ -935,6 +1097,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -998,6 +1170,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -1092,6 +1273,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1530,6 +1717,19 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -1539,11 +1739,12 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hearbit-ai" -version = "0.1.0" +version = "1.1.0" dependencies = [ "chrono", "cpal", "hound", + "lettre", "oauth2", "reqwest 0.11.27", "rubato", @@ -1553,6 +1754,7 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-fs", + "tauri-plugin-log", "tauri-plugin-oauth", "tauri-plugin-opener", "tokio", @@ -1584,6 +1786,17 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.2.1", +] + [[package]] name = "hound" version = "3.5.1" @@ -2086,6 +2299,34 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lettre" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2 0.6.1", + "tokio", + "tokio-native-tls", + "url", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -2163,6 +2404,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "mac" @@ -2367,6 +2611,15 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2433,6 +2686,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "4.4.2" @@ -2715,6 +2977,15 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3238,6 +3509,36 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3256,12 +3557,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3442,6 +3755,15 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -3560,6 +3882,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rubato" version = "0.14.1" @@ -3572,6 +3923,22 @@ dependencies = [ "realfft", ] +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3745,6 +4112,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -4010,6 +4383,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -4125,6 +4504,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "strength_reduce" version = "0.2.4" @@ -4306,6 +4698,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -4494,6 +4892,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + [[package]] name = "tauri-plugin-oauth" version = "2.0.0" @@ -4704,7 +5124,9 @@ checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -4737,6 +5159,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -5177,6 +5614,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5195,6 +5638,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -6111,6 +6560,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b4a8184..0b77826 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hearbit-ai" -version = "0.1.0" +version = "1.1.0" description = "A Tauri App" authors = ["you"] edition = "2021" @@ -34,3 +34,5 @@ rubato = "0.14.1" tauri-plugin-oauth = "2.0.0" oauth2 = "4.4" url = "2.5" +lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls", "builder"] } +tauri-plugin-log = "2.0.0" diff --git a/src-tauri/src/audio_processor.rs b/src-tauri/src/audio_processor.rs index e09f803..263c15f 100644 --- a/src-tauri/src/audio_processor.rs +++ b/src-tauri/src/audio_processor.rs @@ -131,8 +131,15 @@ impl AudioProcessor { while self.vad_buffer.len() >= self.vad_chunk_size { let vad_chunk: Vec = self.vad_buffer.drain(0..self.vad_chunk_size).collect(); // Run Detection - let probability = self.vad.predict(vad_chunk); - let is_speech = probability > 0.5; + // Run Detection + let probability = self.vad.predict(vad_chunk.clone()); + + // Calculate RMS for this chunk to use as fallback/hybrid detection + let sq_sum: f32 = vad_chunk.iter().map(|x| x * x).sum(); + let rms = (sq_sum / vad_chunk.len() as f32).sqrt(); + + // Hybrid VAD: Probability > 0.4 OR RMS > 0.005 (approx -46dB) + let is_speech = probability > 0.4 || rms > 0.005; if is_speech { self.is_speech_active = true; @@ -141,8 +148,14 @@ impl AudioProcessor { // Emit VAD event periodically (every 500ms) if self.last_event_time.elapsed().as_millis() > 500 { + // Calculate simple RMS of the current chunk for debugging + let sq_sum: f32 = vad_chunk.iter().map(|x| x * x).sum(); + let rms = (sq_sum / vad_chunk.len() as f32).sqrt(); + + // Print debug info to stdout (viewable in terminal) + println!("VAD Debug: Prob={:.4}, RMS={:.6}, Speech={}", probability, rms, is_speech); + if let Some(app) = &self.app_handle { - // Calculate crude RMS for visualization or just send probability // Just sending probability is enough for now #[derive(serde::Serialize, Clone)] struct VadEvent { diff --git a/src-tauri/src/email.rs b/src-tauri/src/email.rs new file mode 100644 index 0000000..57b3e21 --- /dev/null +++ b/src-tauri/src/email.rs @@ -0,0 +1,96 @@ +use tauri::AppHandle; +use lettre::{Message, SmtpTransport, Transport, AsyncTransport}; +use lettre::transport::smtp::authentication::Credentials; +use lettre::transport::smtp::AsyncSmtpTransport; +use lettre::Tokio1Executor; + +#[derive(serde::Deserialize)] +pub struct SmtpConfig { + host: String, + port: u16, + username: String, + password: String, + sender_email: String, + sender_name: Option, +} + +#[derive(serde::Deserialize)] +pub struct EmailMessage { + to: Vec, + subject: String, + body_html: String, +} + +#[tauri::command] +pub async fn send_smtp_email(app: AppHandle, config: SmtpConfig, message: EmailMessage) -> Result { + println!("SMTP: Preparing to send email to {:?}", message.to); + + // 1. Build Message + let sender_str = if let Some(name) = &config.sender_name { + format!("{} <{}>", name, config.sender_email) + } else { + config.sender_email.clone() + }; + + let mut builder = Message::builder() + .from(sender_str.parse().map_err(|e: lettre::address::AddressError| e.to_string())?) + .subject(message.subject); + + for recipient in message.to { + builder = builder.to(recipient.parse().map_err(|e: lettre::address::AddressError| e.to_string())?); + } + + let email = builder + .header(lettre::message::header::ContentType::TEXT_HTML) + .body(message.body_html) + .map_err(|e| e.to_string())?; + + // 2. Build Transport + println!("SMTP: Connecting to {}:{}...", config.host, config.port); + let creds = Credentials::new(config.username, config.password); + + // TLS Configuration + // Proton Mail and others on 465 require Implicit TLS (Wrapper). + // Others on 587 use STARTTLS (Opportunistic). + + use lettre::transport::smtp::client::{Tls, TlsParameters}; + + let tls_params = TlsParameters::builder(config.host.clone()) + .build() + .map_err(|e| format!("TLS Params error: {}", e))?; + + let builder = AsyncSmtpTransport::::relay(&config.host) + .map_err(|e| e.to_string())? + .port(config.port) + .credentials(creds); + + let mailer: AsyncSmtpTransport = if config.port == 465 { + println!("SMTP: Using Implicit TLS (Wrapper) for port 465"); + builder.tls(Tls::Wrapper(tls_params)).build() + } else { + println!("SMTP: Using Opportunistic TLS (STARTTLS) for port {}", config.port); + builder.tls(Tls::Opportunistic(tls_params)).build() + }; + + // 3. Send with Timeout + println!("SMTP: Sending..."); + let send_task = mailer.send(email); + + match tokio::time::timeout(std::time::Duration::from_secs(15), send_task).await { + Ok(result) => match result { + Ok(_) => { + println!("SMTP: Success!"); + Ok("Email sent successfully".to_string()) + }, + Err(e) => { + println!("SMTP: Failed: {}", e); + // Hint at common issues + Err(format!("Failed to send: {}. (Check Host/Port/Password)", e)) + } + }, + Err(_) => { + println!("SMTP: Timeout"); + Err("Connection timed out after 15s. Try changing Port (465 vs 587) or check VPN/Firewall.".to_string()) + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5b2f3c7..cadbbf9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ use tokio::time::sleep; mod audio_processor; use audio_processor::AudioProcessor; mod auth; +mod email; // State to hold the active recording stream struct AppState { @@ -625,9 +626,26 @@ async fn save_text_file(app: AppHandle, path: String, content: String) -> Result +#[tauri::command] +async fn read_log_file(app: AppHandle) -> Result { + let log_path = app.path().app_log_dir().map_err(|e| e.to_string())?.join("hearbit-ai.log"); + if log_path.exists() { + let content = std::fs::read_to_string(&log_path).map_err(|e| e.to_string())?; + Ok(content) + } else { + Ok("No log file found yet.".to_string()) + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_log::Builder::default() + .targets([ + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: Some("hearbit-ai.log".to_string()) }), + ]) + .build()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) @@ -650,7 +668,9 @@ pub fn run() { create_hearbit_audio_device, auth::start_auth_flow, auth::get_calendar_events, - save_text_file + save_text_file, + read_log_file, + email::send_smtp_email ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 332e2e9..e375996 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Hearbit AI", - "version": "0.1.0", + "version": "1.1.0", "identifier": "com.hearbit-ai.desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 959d2ec..2b48ed9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,8 @@ -import { useState, useEffect } from 'react'; -import { listen } from "@tauri-apps/api/event"; +import { useState } from 'react'; import { Settings as SettingsIcon } from "lucide-react"; -import Settings from "./components/Settings"; +import Settings, { SmtpConfig, AzureConfig } from "./components/Settings"; import Recorder from "./components/Recorder"; -import LogViewer, { LogEntry } from "./components/LogViewer"; + import TranscriptionView from "./components/TranscriptionView"; import Tabs from "./components/Tabs"; import MeetingsView from "./components/MeetingsView"; @@ -17,9 +16,16 @@ export interface PromptTemplate { keywords?: string[]; } +export interface EmailTemplate { + id: string; + name: string; + subject: string; + body: string; +} + function App() { - const [view, setView] = useState<'recorder' | 'logs' | 'settings' | 'transcription' | 'meetings' | 'history'>('recorder'); - const [lastTab, setLastTab] = useState<'recorder' | 'logs' | 'transcription' | 'meetings' | 'history'>('recorder'); + const [view, setView] = useState<'recorder' | 'settings' | 'transcription' | 'meetings' | 'history'>('recorder'); + const [lastTab, setLastTab] = useState<'recorder' | 'transcription' | 'meetings' | 'history'>('recorder'); // Auto-start recording state to handle "Join & Record" transition @@ -40,6 +46,23 @@ function App() { const [apiKey, setApiKey] = useState(localStorage.getItem('infomaniak_api_key') || ''); const [productId, setProductId] = useState(localStorage.getItem('infomaniak_product_id') || ''); const [savePath, setSavePath] = useState(localStorage.getItem('infomaniak_save_path') || ''); + const [smtpConfig, setSmtpConfig] = useState(() => { + const saved = localStorage.getItem('hearbit_smtp_config'); + return saved ? JSON.parse(saved) : { host: '', port: '587', user: '', pass: '', sender: '', senderName: '' }; + }); + const [azureConfig, setAzureConfig] = useState(() => { + const saved = localStorage.getItem('hearbit_azure_config'); + return saved ? JSON.parse(saved) : { clientId: '', tenantId: '' }; + }); + + const [selectedModel, setSelectedModel] = useState(() => { + return localStorage.getItem('hearbit_selected_model') || 'mixtral'; + }); + + const handleModelChange = (model: string) => { + setSelectedModel(model); + localStorage.setItem('hearbit_selected_model', model); + }; // Default prompts if none exist /* eslint-disable no-useless-escape */ // Escape quotes in prompts @@ -164,20 +187,70 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`, } ]; + // Default Email Templates + const defaultEmailTemplates: EmailTemplate[] = [ + { + id: '1', + name: 'Meeting Summary (Standard)', + subject: 'Meeting Summary: {{subject}}', + body: `Hi everyone, + +Here is the summary of our meeting "{{subject}}" from {{date}}. + +{{summary}} + +Best regards, +Hearbit Assistant` + }, + { + id: '2', + name: 'Action Items Only', + subject: 'Action Items: {{subject}}', + body: `Hi Team, + +Please find below the action items from our call on {{date}}: + +{{summary}} + +Thanks!` + } + ]; + const [prompts, setPrompts] = useState(() => { const saved = localStorage.getItem('infomaniak_prompts'); return saved ? JSON.parse(saved) : defaultPrompts; }); - const handleSaveSettings = (newApiKey: string, newProductId: string, newPrompts: PromptTemplate[], newSavePath: string) => { + const [emailTemplates, setEmailTemplates] = useState(() => { + const saved = localStorage.getItem('hearbit_email_templates'); + return saved ? JSON.parse(saved) : defaultEmailTemplates; + }); + + const handleSaveSettings = ( + newApiKey: string, + newProductId: string, + newPrompts: PromptTemplate[], + newSavePath: string, + newSmtp: SmtpConfig, + newAzure: AzureConfig, + newEmailTemplates: EmailTemplate[] + ) => { setApiKey(newApiKey); setProductId(newProductId); setPrompts(newPrompts); setSavePath(newSavePath); + setSmtpConfig(newSmtp); + setAzureConfig(newAzure); + setEmailTemplates(newEmailTemplates); + localStorage.setItem('infomaniak_api_key', newApiKey); localStorage.setItem('infomaniak_product_id', newProductId); localStorage.setItem('infomaniak_prompts', JSON.stringify(newPrompts)); localStorage.setItem('infomaniak_save_path', newSavePath); + localStorage.setItem('hearbit_smtp_config', JSON.stringify(newSmtp)); + localStorage.setItem('hearbit_azure_config', JSON.stringify(newAzure)); + localStorage.setItem('hearbit_email_templates', JSON.stringify(newEmailTemplates)); + setView(lastTab); }; @@ -250,18 +323,7 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`, setView('transcription'); // Switch to Transcription view to see content }; - // Logs State - const [logs, setLogs] = useState([]); - useEffect(() => { - const unlisten = listen('log-event', (event) => { - setLogs((prevLogs) => [...prevLogs, event.payload]); - }); - - return () => { - unlisten.then(f => f()); - }; - }, []); return (
@@ -271,7 +333,7 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`,
setView(t)} />
@@ -313,11 +375,34 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`, recordingSubject={recordingSubject} onAutoStartHandled={() => setAutoStartRecording(false)} addToast={addToast} + selectedModel={selectedModel} + onModelChange={handleModelChange} /> )} {view === 'transcription' && ( - + { + setSummary(newSummary); // Update view + // Also update history item if it exists + // We identify by transcription content match (simple heuristic) or we should track currentId + const histIdx = history.findIndex(h => h.transcription === transcription); + if (histIdx >= 0) { + const newHist = [...history]; + newHist[histIdx] = { ...newHist[histIdx], summary: newSummary }; + setHistory(newHist); + localStorage.setItem('infomaniak_history', JSON.stringify(newHist)); + } + }} + addToast={addToast} + /> )} {view === 'history' && ( @@ -329,18 +414,22 @@ Wann findet das nächste Meeting statt oder was ist der nächste Meilenstein?`, )} {view === 'meetings' && ( + { setView('recorder'); setRecordingSubject(subject || ''); setAutoStartRecording(true); }} + apiKey={apiKey} + productId={productId} + selectedModel={selectedModel} + onModelChange={handleModelChange} /> )} - {view === 'logs' && ( - - )} + {view === 'settings' && ( )} diff --git a/src/components/EmailPreviewModal.tsx b/src/components/EmailPreviewModal.tsx new file mode 100644 index 0000000..929b3cd --- /dev/null +++ b/src/components/EmailPreviewModal.tsx @@ -0,0 +1,280 @@ +import React, { useState, useEffect } from 'react'; +import { Mail, X, Send } from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; + +import { EmailTemplate } from '../App'; + +interface EmailPreviewModalProps { + isOpen: boolean; + onClose: () => void; + initialRecipients: string[]; + initialSubject: string; + initialBody: string; + emailTemplates: EmailTemplate[]; // New prop + smtpConfig: { + host: string; + port: number; + user: string; + pass: string; + sender: string; + senderName?: string; + } | null; + addToast: (msg: string, type: 'success' | 'error' | 'info') => void; +} + +// Basic Markdown to HTML converter for email body +const formatMarkdownToHtml = (markdown: string): string => { + let html = markdown + // Headers + .replace(/^# (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^### (.*$)/gim, '

$1

') + + // Bold + .replace(/\*\*(.*?)\*\*/gim, '$1') + + // Lists + .replace(/^\s*-\s+(.*$)/gim, '
  • $1
  • ') + // Wrap lists (simple heuristic) + .replace(/()/gim, '
      $1
    ') + .replace(/<\/ul>\s*
      /gim, '') // Merge adjacent lists + + // Tables (Basic support for the format used in prompts) + .replace(/\| (.*?) \| (.*?) \| (.*?) \|/gim, '$1$2$3') + .replace(/\| :--- \| :--- \| :--- \|/gim, '') // Remove separator row + // Tables wrapping (heuristic) + .replace(/(.*<\/tr>)/gim, '$1
      ') + .replace(/<\/table>\s*/gim, '') // Merge adjacent tables + + // Paragraphs (double newlines) + .replace(/\n\n/gim, '

      '); + + return html; +}; + +const EmailPreviewModal: React.FC = ({ + isOpen, onClose, initialRecipients, initialSubject, initialBody, smtpConfig, addToast, emailTemplates +}) => { + const [recipients, setRecipients] = useState(initialRecipients.join(', ')); + const [subject, setSubject] = useState(initialSubject); + const [body, setBody] = useState(initialBody); + const [sending, setSending] = useState(false); + const [selectedTemplateId, setSelectedTemplateId] = useState(''); + + const [activeTab, setActiveTab] = useState<'preview' | 'source'>('preview'); + + const generateHtmlBody = (content: string, title: string) => { + // Simple heuristic: if it looks like HTML, treat as HTML. Otherwise, markdown. + const isHtml = /^\s*<(!DOCTYPE|html|div|p|table)/i.test(content); + const formattedBody = isHtml ? content : formatMarkdownToHtml(content); + + return ` + + + +
      + +
      +

      ${title}

      +
      + +
      + ${formattedBody} +
      + +
      +

      + Generated by Hearbit AI • Secure & Local Meeting Intelligence +

      +
      +
      + +`; + }; + + useEffect(() => { + if (isOpen) { + setRecipients(initialRecipients.join(', ')); + setSubject(initialSubject); + // Default: Wrap initialBody in our HTML template + setBody(generateHtmlBody(initialBody, initialSubject)); + setSelectedTemplateId(''); + } + }, [isOpen, initialRecipients, initialSubject, initialBody]); + + const handleTemplateChange = (tmplId: string) => { + setSelectedTemplateId(tmplId); + if (!tmplId) return; + + const tmpl = emailTemplates.find(t => t.id === tmplId); + if (tmpl) { + // Replace placeholders + const dateStr = new Date().toLocaleDateString(); + let newSub = tmpl.subject.replace(/{{date}}/g, dateStr).replace(/{{subject}}/g, "Meeting"); + // Note: We don't have the original 'recordingSubject' here easily without more prop drilling, + // so we default to "Meeting" or user can edit. + // Actually, initialSubject usually contains "Meeting Summary", so we could parse it, but for now date/summary is most important. + + let newBody = tmpl.body + .replace(/{{date}}/g, dateStr) + .replace(/{{subject}}/g, "the meeting") + .replace(/{{summary}}/g, initialBody); + + setSubject(newSub); + setBody(generateHtmlBody(newBody, newSub)); + } + }; + + if (!isOpen) return null; + + const handleSend = async () => { + if (!smtpConfig || !smtpConfig.host) { + addToast("SMTP Settings not configured. Please go to Settings.", "error"); + return; + } + + setSending(true); + try { + // Split recipients by comma/semicolon and clean + const toList = recipients.split(/[,;]/).map(s => s.trim()).filter(s => s.length > 0); + + await invoke('send_smtp_email', { + config: { + host: smtpConfig.host, + port: Number(smtpConfig.port), + username: smtpConfig.user, + password: smtpConfig.pass, + sender_email: smtpConfig.sender, + sender_name: smtpConfig.senderName + }, + message: { + to: toList, + subject: subject, + body_html: body + } + }); + addToast("Email sent successfully!", "success"); + onClose(); + } catch (e) { + console.error(e); + addToast(`Failed to send email: ${e}`, "error"); + } finally { + setSending(false); + } + }; + + return ( +
      +
      +
      +

      + Send Summary via Email +

      + +
      + +
      + {/* Template Selector */} + {emailTemplates.length > 0 && ( +
      + + +
      + )} + +
      +
      + + setRecipients(e.target.value)} + className="w-full p-2 rounded border border-border bg-secondary text-sm focus:ring-2 focus:ring-primary outline-none" + placeholder="email@example.com, other@example.com" + /> +
      +
      + + setSubject(e.target.value)} + className="w-full p-2 rounded border border-border bg-secondary text-sm focus:ring-2 focus:ring-primary outline-none" + /> +
      +
      + +
      + {/* Tabs */} +
      + + +
      + + {/* Content */} +
      + {activeTab === 'preview' ? ( +
      +