Fix automatic translation request handling
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
RuDS is a native Rust blogging desktop application and the successor to bDS2. It manages local projects from authoring through preview, static-site generation, integrity checks, and publishing while preserving the existing bDS filesystem and SQLite formats.
|
RuDS is a native Rust blogging desktop application and the successor to bDS2. It manages local projects from authoring through preview, static-site generation, integrity checks, and publishing while preserving the existing bDS filesystem and SQLite formats.
|
||||||
|
|
||||||
The desktop is a single-window application: closing its window persists UI state and exits RuDS. Background notifications remain unobtrusive and preserve keyboard focus while editing.
|
The desktop is a single-window application: closing its window persists UI state and exits RuDS. Background notifications remain unobtrusive and preserve keyboard focus while editing; errors stay visible until dismissed.
|
||||||
|
|
||||||
The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately.
|
The project is under active development. Core blogging workflows are broadly available; remaining core work and optional extensions are tracked separately.
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,42 @@ pub struct OneShotRequest {
|
|||||||
pub content: Value,
|
pub content: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn post_translation_request(
|
||||||
|
title: &str,
|
||||||
|
excerpt: Option<&str>,
|
||||||
|
content: &str,
|
||||||
|
target_language: &str,
|
||||||
|
) -> OneShotRequest {
|
||||||
|
OneShotRequest {
|
||||||
|
operation: OneShotOperation::TranslatePost {
|
||||||
|
target_language: target_language.to_string(),
|
||||||
|
},
|
||||||
|
content: json!({
|
||||||
|
"title": title,
|
||||||
|
"excerpt": excerpt.unwrap_or_default(),
|
||||||
|
"content": content,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn media_translation_request(
|
||||||
|
title: Option<&str>,
|
||||||
|
alt: Option<&str>,
|
||||||
|
caption: Option<&str>,
|
||||||
|
target_language: &str,
|
||||||
|
) -> OneShotRequest {
|
||||||
|
OneShotRequest {
|
||||||
|
operation: OneShotOperation::TranslateMedia {
|
||||||
|
target_language: target_language.to_string(),
|
||||||
|
},
|
||||||
|
content: json!({
|
||||||
|
"title": title.unwrap_or_default(),
|
||||||
|
"alt": alt.unwrap_or_default(),
|
||||||
|
"caption": caption.unwrap_or_default(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct TaxonomySuggestion {
|
pub struct TaxonomySuggestion {
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
@@ -1477,6 +1513,33 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translation_requests_normalize_missing_fields() {
|
||||||
|
let post = post_translation_request("Hello", None, "Body", "de");
|
||||||
|
assert_eq!(
|
||||||
|
post.operation,
|
||||||
|
OneShotOperation::TranslatePost {
|
||||||
|
target_language: "de".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
post.content,
|
||||||
|
json!({"title": "Hello", "excerpt": "", "content": "Body"})
|
||||||
|
);
|
||||||
|
|
||||||
|
let media = media_translation_request(None, None, None, "fr");
|
||||||
|
assert_eq!(
|
||||||
|
media.operation,
|
||||||
|
OneShotOperation::TranslateMedia {
|
||||||
|
target_language: "fr".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
media.content,
|
||||||
|
json!({"title": "", "alt": "", "caption": ""})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_one_shot_supports_image_analysis_via_airplane_endpoint() {
|
fn run_one_shot_supports_image_analysis_via_airplane_endpoint() {
|
||||||
let response = run_airplane_one_shot(
|
let response = run_airplane_one_shot(
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ use std::collections::HashSet;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::db::DbConnection as Connection;
|
use crate::db::DbConnection as Connection;
|
||||||
use crate::db::queries::{
|
use crate::db::queries::{
|
||||||
media as qm, media_translation as qmt, post as qp, post_media, post_translation,
|
media as qm, media_translation as qmt, post as qp, post_media, post_translation,
|
||||||
};
|
};
|
||||||
use crate::engine::ai::{
|
use crate::engine::ai::{self, MediaTranslationResult, OneShotResponse, TranslationResult};
|
||||||
self, MediaTranslationResult, OneShotOperation, OneShotRequest, OneShotResponse,
|
|
||||||
TranslationResult,
|
|
||||||
};
|
|
||||||
use crate::engine::{EngineError, EngineResult};
|
use crate::engine::{EngineError, EngineResult};
|
||||||
use crate::i18n::{UiLocale, translate, translate_with};
|
use crate::i18n::{UiLocale, translate, translate_with};
|
||||||
use crate::model::{Media, Post, PostStatus};
|
use crate::model::{Media, Post, PostStatus};
|
||||||
@@ -445,16 +440,12 @@ fn translate_post_ai(
|
|||||||
match ai::run_one_shot(
|
match ai::run_one_shot(
|
||||||
conn,
|
conn,
|
||||||
offline_mode,
|
offline_mode,
|
||||||
&OneShotRequest {
|
&ai::post_translation_request(
|
||||||
operation: OneShotOperation::TranslatePost {
|
&post.title,
|
||||||
target_language: language.to_string(),
|
post.excerpt.as_deref(),
|
||||||
},
|
post.content.as_deref().unwrap_or_default(),
|
||||||
content: json!({
|
language,
|
||||||
"title": post.title,
|
),
|
||||||
"excerpt": post.excerpt,
|
|
||||||
"content": post.content,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)? {
|
)? {
|
||||||
(OneShotResponse::Translation(result), _usage) => Ok(result),
|
(OneShotResponse::Translation(result), _usage) => Ok(result),
|
||||||
_ => Err(EngineError::Parse(
|
_ => Err(EngineError::Parse(
|
||||||
@@ -472,16 +463,12 @@ fn translate_media_ai(
|
|||||||
match ai::run_one_shot(
|
match ai::run_one_shot(
|
||||||
conn,
|
conn,
|
||||||
offline_mode,
|
offline_mode,
|
||||||
&OneShotRequest {
|
&ai::media_translation_request(
|
||||||
operation: OneShotOperation::TranslateMedia {
|
media.title.as_deref(),
|
||||||
target_language: language.to_string(),
|
media.alt.as_deref(),
|
||||||
},
|
media.caption.as_deref(),
|
||||||
content: json!({
|
language,
|
||||||
"title": media.title,
|
),
|
||||||
"alt": media.alt,
|
|
||||||
"caption": media.caption,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)? {
|
)? {
|
||||||
(OneShotResponse::MediaTranslation(result), _usage) => Ok(result),
|
(OneShotResponse::MediaTranslation(result), _usage) => Ok(result),
|
||||||
_ => Err(EngineError::Parse(
|
_ => Err(EngineError::Parse(
|
||||||
|
|||||||
@@ -10367,16 +10367,12 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = ai::OneShotRequest {
|
let request = ai::post_translation_request(
|
||||||
operation: ai::OneShotOperation::TranslatePost {
|
&state.title,
|
||||||
target_language: target_language.to_string(),
|
Some(&state.excerpt),
|
||||||
},
|
&state.content,
|
||||||
content: json!({
|
target_language,
|
||||||
"title": state.title,
|
);
|
||||||
"excerpt": state.excerpt,
|
|
||||||
"content": state.content,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
if let Some(editor) = self.post_editors.get_mut(post_id) {
|
if let Some(editor) = self.post_editors.get_mut(post_id) {
|
||||||
editor.ai_activity = Some(t(self.ui_locale, "editor.translate"));
|
editor.ai_activity = Some(t(self.ui_locale, "editor.translate"));
|
||||||
}
|
}
|
||||||
@@ -10513,16 +10509,12 @@ impl BdsApp {
|
|||||||
{
|
{
|
||||||
editor.language = state.canonical_language.clone();
|
editor.language = state.canonical_language.clone();
|
||||||
}
|
}
|
||||||
let request = ai::OneShotRequest {
|
let request = ai::media_translation_request(
|
||||||
operation: ai::OneShotOperation::TranslateMedia {
|
Some(&state.title),
|
||||||
target_language: target_language.to_string(),
|
Some(&state.alt),
|
||||||
},
|
Some(&state.caption),
|
||||||
content: json!({
|
target_language,
|
||||||
"title": state.title,
|
);
|
||||||
"alt": state.alt,
|
|
||||||
"caption": state.caption,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
if let Some(editor) = self.media_editors.get_mut(media_id) {
|
if let Some(editor) = self.media_editors.get_mut(media_id) {
|
||||||
editor.ai_activity = Some(t(self.ui_locale, "editor.translate"));
|
editor.ai_activity = Some(t(self.ui_locale, "editor.translate"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/// Toast notification state.
|
/// Toast notification state.
|
||||||
///
|
///
|
||||||
/// Toasts are ephemeral, auto-dismissing messages shown at the top of
|
/// Toasts are messages shown at the top of the workspace. Errors remain until
|
||||||
/// the workspace. Each toast has a severity level, a message, and a
|
/// dismissed; other levels expire automatically.
|
||||||
/// monotonically increasing id used for targeted dismissal.
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
static NEXT_TOAST_ID: AtomicU64 = AtomicU64::new(1);
|
static NEXT_TOAST_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
@@ -45,6 +44,9 @@ impl Toast {
|
|||||||
|
|
||||||
/// Whether this toast has exceeded its display duration.
|
/// Whether this toast has exceeded its display duration.
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
|
if self.level == ToastLevel::Error {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -69,4 +71,11 @@ mod tests {
|
|||||||
let t = Toast::new(ToastLevel::Info, "test".into());
|
let t = Toast::new(ToastLevel::Info, "test".into());
|
||||||
assert!(!t.is_expired());
|
assert!(!t.is_expired());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_toast_never_expires_automatically() {
|
||||||
|
let mut toast = Toast::new(ToastLevel::Error, "important".into());
|
||||||
|
toast.created_at = 0;
|
||||||
|
assert!(!toast.is_expired());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user