Belajar Desktop Developer - AI-Powered Desktop Apps
Episode 21 of 28

Belajar Desktop Developer - AI-Powered Desktop Apps

Menambahkan kecerdasan ke CatatanKu: integrasi LLM cloud dengan token aman, menjalankan model AI lokal di perangkat pengguna, dan merancang fitur AI — rangkum otomatis dan chat dengan catatan — dengan UI yang tidak pernah membeku

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Di episode 1 kita menyebut AI lokal sebagai alasan desktop makin relevan di 2026. Sekarang kita buktikan. Aplikasi desktop punya posisi unik untuk AI: ia bisa membundel atau menjalankan model lokal, mengakses data pengguna secara penuh, dan menampilkan interaksi yang panjang tanpa dibatasi tab browser.

Episode ini menambahkan AI ke CatatanKu dengan dua jalur: LLM cloud (kekuatan model besar, dengan token dikelola lewat pola episode 18-19) dan model lokal (privasi penuh, tanpa jaringan). Fitur yang kita bangun: rangkuman otomatis catatan dan chat dengan catatan (RAG sederhana). Kita juga membahas pola UI untuk stream output yang panjang — tanpa membekukan aplikasi.

Arsitektur AI di Desktop

Dua Jalur, Dua Trade-Off

100%
AspekLLM CloudModel Lokal
KualitasModel besar (GPT/Claude/Gemini)Tergantung model (umumnya lebih kecil)
PrivasiData keluar perangkatData tetap lokal
JaringanWajibOpsional
BiayaPer tokenGratis, tapi pakai CPU/RAM
IntegrasiAPI + tokenRuntime model di dalam aplikasi

Strategi CatatanKu: default ke cloud jika pengguna memilih dan menyetujui; opsi model lokal untuk mode offline/private. Keduanya dibungkus di balik satu antarmuka ai/complete — pola abstraction yang sama seperti storage di episode 8.

Integrasi LLM Cloud

Token Aman, Request via Backend

Konsisten dengan episode 18: LLM dipanggil dari backend, token disimpan di keychain (episode 19), dan webview tidak pernah melihat kunci:

Tauri: panggil LLM cloud dari backend
#[tauri::command]
async fn summarize_note(app: AppHandle, note_id: String) -> Result<String, String> {
    let token = get_llm_token(&app)?;                    // keychain
    let content = load_note(&app, &note_id)?;            // database
    let prompt = format!(
        "Rangkum catatan berikut dalam 3 poin:\n\n{content}"
    );
    let client = reqwest::Client::new();
    let resp = client
        .post("https://api.catatanku.app/ai/complete")
        .header("Authorization", format!("Bearer {token}"))
        .json(&json!({ "model": "default", "messages": [
            { "role": "user", "content": prompt }
        ]}))
        .send().await.map_err(|e| e.to_string())?;
    resp.json::<Completion>().await.map_err(|e| e.to_string())
        .map(|c| c.choices[0].message.content.clone())
}

Pola yang sama di Electron: net.request dari main process dengan token dari safeStorage/keytar. Token LLM tidak pernah ada di renderer — jika webview dikompromi (episode 18), kunci AI tidak ikut bocor.

Note

Untuk provider eksternal (OpenAI, Anthropic, Gemini), panggil API mereka langsung dari backend kalian — jangan pernah memaparkan API key pengguna ke UI. Pertimbangkan juga melewati server kalian sendiri agar bisa membatasi rate, mengaudit, dan menambah kebijakan privasi terpusat.

Model AI Lokal

Privasi Penuh, Tanpa Cloud

Untuk pengguna yang menuntut privasi penuh (atau bekerja offline), CatatanKu menyediakan model lokal. Pilihan praktisnya:

  • ONNX Runtime — model terkonversi, terintegrasi dalam aplikasi, berjalan di CPU/GPU.
  • llama.cpp binding (via Tauri Rust crate atau FFI) — efisien untuk model bahasa.
  • Ollama — runtime terpisah; aplikasi memanggil API lokal http://localhost:11434.
Tauri: jalankan inference lokal (ONNX)
use ort::{Session, inputs};
 
#[tauri::command]
async fn local_summarize(content: String) -> Result<String, String> {
    let model = Session::builder()
        .commit_from_file("models/rangkum.onnx")
        .map_err(|e| e.to_string())?;
 
    // tokenize + infer di thread terpisah (episode 9)
    let out = model.run(inputs!["input" => tokens]?)
        .map_err(|e| e.to_string())?;
    decode_output(out)
}

Menjalankan model di thread utama = UI membeku beberapa detik. Selalu jalankan inference di worker/isolate (episode 9) dan stream hasil bertahap.

Kompromi Ukuran

Model lokal berukuran 100 MB-8 GB. Keputusan desain: bundling (aplikasi besar, siap pakai) vs download on demand (aplikasi kecil, unduh saat fitur AI lokal diminta) vs bring your own model (pengguna memilih jalur model). CatatanKu memakai download on demand — konsisten dengan strategi update episode 13 dan menghindari installer raksasa.

Fitur AI: Rangkum dan Chat dengan Catatan

Rangkum Otomatis

Fitur pertama: tombol "Rangkum" di editor menghasilkan ringkasan catatan. Logika-nya: ambil isi catatan (backend), kirim prompt ke LLM (cloud atau lokal), tampilkan hasil dalam panel dengan status streaming.

Chat dengan Catatan (RAG Sederhana)

Fitur kedua lebih menarik: pengguna bertanya "apa poin penting dari catatan minggu lalu?" Jawaban butuh konteks dari banyak catatan — pola Retrieval-Augmented Generation (RAG):

Alur RAG di CatatanKu
1. Pengguna bertanya → embedding query (model embedding lokal)
2. Cari catatan relevan: cosine similarity atas embedding tersimpan
3. Gabungkan: top-k catatan + pertanyaan → prompt ke LLM
4. Jawab dengan konteks catatan yang relevan

Embedding catatan dihitung di latar belakang (episode 9) saat indexing FTS (episode 8) — satu pipeline yang mengisi vektor store lokal (SQLite dengan ekstensi vektor, atau penyimpanan terpisah).

UI Streaming: Tidak Membeku

Pola Output Bertahap

LLM menghasilkan teks token per token — menunggu selesai sebelum menampilkan = UI diam selama puluhan detik. Pola yang benar: streaming event dari backend ke UI, update bertahap:

Tauri: stream hasil AI ke UI
#[tauri::command]
async fn chat(app: AppHandle, query: String) -> Result<(), String> {
    let stream = llm_stream(app, query).await?;
    let mut acc = String::new();
    for chunk in stream {
        acc.push_str(&chunk);
        let _ = app.emit("ai:chunk", chunk);
    }
    let _ = app.emit("ai:done", acc);
    Ok(())
}
Frontend: tampilkan streaming
await listen<string>("ai:chunk", ({ payload }) => {
  setAnswer((prev) => prev + payload);   // render tiap chunk
});

Throttling tetap berlaku (episode 9): render chunk dalam batch kecil, jangan per karakter per event pada teks panjang.

Common Pitfalls

  • Token LLM di frontend — bocor saat webview dikompromi; selalu backend + keychain.
  • Inference di thread UI — UI beku detik/ menit; selalu worker.
  • Prompt injection — isi catatan pengguna bisa berisi instruksi berbahaya; batasi peran model dan validasi output.
  • Data sensitif ke cloud tanpa consent — episode 20: minta persetujuan sebelum mengirim isi catatan ke LLM cloud.
  • Memperlakukan model lokal sebagai gratis total — model besar bisa menguras RAM/CPU; ukur impact di episode 16.

Penutup

Inti yang harus dibawa pulang:

  • AI desktop punya dua jalur: LLM cloud (kuat, butuh consent & token aman) vs model lokal (privasi penuh, butuh resource).
  • Token AI disimpan di keychain dan request lewat backend — tidak pernah di webview.
  • RAG (embedding + pencarian relevan) memungkinkan chat dengan catatan pengguna.
  • Streaming output (event chunk) mencegah UI membeku saat menunggu model.
  • Model lokal dijalankan di worker, didownload on demand, dan membutuhkan consent sesuai episode 20.

Di episode 22 selanjutnya kita menyempurnakan offline-first CatatanKu: local-first & sync — CRDT, sinkronisasi antar perangkat, dan kolaborasi offline. Sampai jumpa di episode 22!