// Tauri commands for human-in-the-loop feedback capture and retrieval. // Phase 2 of the feature-complete roadmap: thumbs + correction capture // on AI-generated output feeds a few-shot loop that conditions future // prompts on the user's preferred style. use serde::{Deserialize, Serialize}; use kon_storage::{ list_feedback_examples as db_list_feedback_examples, record_feedback as db_record_feedback, FeedbackRow, FeedbackTargetType, RecordFeedbackParams, }; use crate::AppState; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RecordFeedbackInput { /// One of "microstep", "task_extraction", "cleanup". pub target_type: String, /// Optional surface-specific id (subtask id, task id, transcript id). #[serde(default)] pub target_id: Option, /// -1 = thumbs down, 0 = correction (neutral), +1 = thumbs up. pub rating: i8, #[serde(default)] pub original_text: Option, #[serde(default)] pub corrected_text: Option, /// Freeform JSON context: e.g. the parent task text, the transcript /// chunk the AI was given, etc. Used later by the prompt builder /// to reconstruct the (input, preferred-output) pair. #[serde(default)] pub context_json: Option, #[serde(default)] pub profile_id: Option, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct FeedbackDto { pub id: i64, pub target_type: String, pub target_id: Option, pub rating: i64, pub original_text: Option, pub corrected_text: Option, pub context_json: Option, pub profile_id: String, pub created_at: String, } impl From for FeedbackDto { fn from(r: FeedbackRow) -> Self { Self { id: r.id, target_type: r.target_type, target_id: r.target_id, rating: r.rating, original_text: r.original_text, corrected_text: r.corrected_text, context_json: r.context_json, profile_id: r.profile_id, created_at: r.created_at, } } } fn parse_target_type(raw: &str) -> Result { FeedbackTargetType::parse(raw).ok_or_else(|| format!("unknown feedback target_type: {raw}")) } #[tauri::command] pub async fn record_feedback( state: tauri::State<'_, AppState>, input: RecordFeedbackInput, ) -> Result { let target_type = parse_target_type(&input.target_type)?; db_record_feedback( &state.db, RecordFeedbackParams { target_type, target_id: input.target_id, rating: input.rating, original_text: input.original_text, corrected_text: input.corrected_text, context_json: input.context_json, profile_id: input.profile_id, }, ) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn list_feedback_examples_cmd( state: tauri::State<'_, AppState>, target_type: String, limit: Option, min_rating: Option, profile_id: Option, ) -> Result, String> { let target = parse_target_type(&target_type)?; let limit = limit.unwrap_or(8).clamp(1, 64); let min_rating = min_rating.unwrap_or(0).clamp(-1, 1); let rows = db_list_feedback_examples(&state.db, target, limit, min_rating, profile_id.as_deref()) .await .map_err(|e| e.to_string())?; Ok(rows.into_iter().map(FeedbackDto::from).collect()) }