Add Mistral plot-beat generation (Tools menu)
New Tools ▸ "Generate plot beats (Mistral)…" command: pick a novel proposal markdown file (characters + loose plot summary) and have the Mistral chat API lay it out as plot beats in the classic three-act structure. The result opens in a floating window where it can be edited, saved as a new "<proposal> — beats.md" workspace file, or copied. - src/mistral.rs: pure-Rust ureq/rustls POST to /v1/chat/completions (self-contained; no C bindings). System prompt fixes the three-act markdown shape; friendly API-error surfacing. Unit tests for content parsing, empty/blank responses, error-message extraction, and the missing-key guard. - config: mistral_api_key / mistral_model / mistral_base_url with effective-value helpers and defaults (mistral-large-latest, https://api.mistral.ai). - app: background generation + poll (editor stays responsive), a Settings ▸ Mistral… window (key/model/base URL), and the results window with save/copy. - Docs: README "Plot beats (Mistral)" section and help cheatsheet note. Verified: cargo build --release clean, 56 tests pass (+6 Mistral), ldd still only libc/libgcc/libm. Clippy: no new warnings in the added code (the two extra vs. the old 3-warning baseline are toolchain-surfaced in pre-existing app.rs code). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDfaEsTgcn61n3wgP62DFF
This commit is contained in:
@@ -53,6 +53,26 @@ pub struct Config {
|
||||
/// a [`crate::spell::DictEntry::id`].
|
||||
#[serde(default = "default_spell_language")]
|
||||
pub spell_language: String,
|
||||
/// Mistral API key for plot-beat generation. Stored in plain text; empty
|
||||
/// disables the feature until set.
|
||||
#[serde(default)]
|
||||
pub mistral_api_key: String,
|
||||
/// Mistral model id used for plot-beat generation (blank = the built-in default).
|
||||
#[serde(default = "default_mistral_model")]
|
||||
pub mistral_model: String,
|
||||
/// Mistral API base URL (blank = the built-in default). Override for a proxy.
|
||||
#[serde(default = "default_mistral_base_url")]
|
||||
pub mistral_base_url: String,
|
||||
}
|
||||
|
||||
/// Default Mistral model.
|
||||
pub fn default_mistral_model() -> String {
|
||||
crate::mistral::DEFAULT_MODEL.to_string()
|
||||
}
|
||||
|
||||
/// Default Mistral API base URL.
|
||||
pub fn default_mistral_base_url() -> String {
|
||||
crate::mistral::DEFAULT_BASE_URL.to_string()
|
||||
}
|
||||
|
||||
/// Live spell checking is on by default.
|
||||
@@ -109,6 +129,9 @@ impl Default for Config {
|
||||
languagetool_language: default_languagetool_language(),
|
||||
spell_check: default_spell_check(),
|
||||
spell_language: default_spell_language(),
|
||||
mistral_api_key: String::new(),
|
||||
mistral_model: default_mistral_model(),
|
||||
mistral_base_url: default_mistral_base_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +165,26 @@ impl Config {
|
||||
)
|
||||
}
|
||||
|
||||
/// The Mistral model to use, falling back to the built-in default when blank.
|
||||
pub fn mistral_effective_model(&self) -> String {
|
||||
let m = self.mistral_model.trim();
|
||||
if m.is_empty() {
|
||||
crate::mistral::DEFAULT_MODEL.to_string()
|
||||
} else {
|
||||
m.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The Mistral base URL to use, falling back to the built-in default when blank.
|
||||
pub fn mistral_effective_base_url(&self) -> String {
|
||||
let b = self.mistral_base_url.trim().trim_end_matches('/');
|
||||
if b.is_empty() {
|
||||
crate::mistral::DEFAULT_BASE_URL.to_string()
|
||||
} else {
|
||||
b.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Config {
|
||||
let path = config_path();
|
||||
match std::fs::read_to_string(&path) {
|
||||
|
||||
Reference in New Issue
Block a user