Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(dictionary): extract dictionary trait #3

Merged
merged 2 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ just build

### Add plugin to logseq
- Click the three-dots menu, go to Settings and turn on Developer Mode. After that, there will be a plugins button inside the menu. Click it to go to the plugin list page.
- In the plugin list page, click Load unpacked plugin and choose the `logseq-plugin-vocabulary-card/plugin/dist` folder.
- In the plugin list page, click Load unpacked plugin and choose the `logseq-plugin-vocabulary-card/plugin/` folder.
17 changes: 9 additions & 8 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
[package]
name = "logseq-plugin-vocabulary-card"
version = "0.1.0"
version = "0.1.1"
authors = ["Richard Hao <[email protected]>"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.89"
wasm-bindgen-futures = "0.4.39"
serde-wasm-bindgen = "0.6.3"
serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0.110"
reqwest = { version = "0.11.23", features = ["blocking", "json"] }
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
serde-wasm-bindgen = "0.6.5"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
reqwest = { version = "0.12.5", features = ["blocking", "json"] }
async-trait = "0.1.81"

[dev-dependencies]
wasm-bindgen-test = "0.3.39"
wasm-bindgen-test = "0.3.42"

[profile.release]
# Tell `rustc` to optimize for small code size.
Expand Down
51 changes: 51 additions & 0 deletions lib/src/gemini_dictionary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use async_trait::async_trait;

use crate::{http_request, Dictionary, WordDefinition};

pub struct GeminiDictionary {
api_key: String,
}

impl GeminiDictionary {
pub fn new(api_key: &str) -> Self {
Self {
api_key: api_key.trim().to_string(),
}
}
}

#[async_trait(?Send)]
impl Dictionary for GeminiDictionary {
async fn define(&self, word: &str) -> Result<WordDefinition, String> {
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={}",
self.api_key
);
let req_body = serde_json::json!({
"contents": [{
"parts": [{
"text": self.default_prompt(word)
}]
}]
});

let res_body = http_request::make_request(&url, req_body).await?;
let dictionary = extract_dictionary(res_body)?;
Ok(dictionary)
}
}

fn extract_dictionary(res_body: serde_json::Value) -> Result<WordDefinition, String> {
let Some(dictionary_str) = res_body["candidates"][0]["content"]["parts"][0]["text"].as_str()
else {
return Err("json error: dictionary not found".to_string());
};
let dictionary_str = dictionary_str
.trim_start_matches("```json")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
let definition = serde_json::from_str::<WordDefinition>(dictionary_str)
.map_err(|e| format!("serde error: {}", e))?;
Ok(definition)
}
19 changes: 19 additions & 0 deletions lib/src/http_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use reqwest::StatusCode;

pub async fn make_request(url: &str, body: serde_json::Value) -> Result<serde_json::Value, String> {
let req_client = reqwest::Client::new();
let res = req_client
.post(url)
.json(&body)
.send()
.await
.map_err(|e| format!("request error: {}", e))?;
if res.status() != StatusCode::OK {
return Err(format!("request error: {}", res.status()));
}
let json = res
.json::<serde_json::Value>()
.await
.map_err(|e| format!("json error: {}", e))?;
Ok(json)
}
77 changes: 22 additions & 55 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,73 +1,40 @@
use reqwest::StatusCode;
mod gemini_dictionary;
mod http_request;

use async_trait::async_trait;
use gemini_dictionary::GeminiDictionary;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
#[derive(Serialize, Deserialize)]
pub struct Word {
pub struct WordDefinition {
word: String,
pronunciation: String,
definition: String,
examples: Vec<String>,
image: String,
}

#[wasm_bindgen]
pub async fn define_word(word: &str, api_key: &str) -> Result<JsValue, JsValue> {
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={}",
api_key.trim()
);
let prompt = format!(
"please act as a dictionary, including the pronunciation, explanation, two examples of sentences, and one image. And the word is {}. please output the result in json format, the json keys are `word`, `pronunciation`, `definition`, `examples`, and `image`.",
word.trim()
);
let req_body = serde_json::json!({
"contents": [{
"parts": [{
"text": prompt
}]
}]

});

let res_body = make_request(&url, req_body)
.await
.map_err(|e| JsValue::from_str(&e))?;
let dictionary = extract_dictionary(res_body).map_err(|e| JsValue::from_str(&e))?;
const PROMPT_TEMPLATE: &str = "please act as a dictionary, including the pronunciation, explanation, two examples of sentences, and one image. And the word is `{:word}`. please output the result in json format, the json keys are `word`, `pronunciation`, `definition`, `examples`, and `image`.";

Ok(serde_wasm_bindgen::to_value(&dictionary)?)
#[async_trait(?Send)]
pub trait Dictionary {
// fn set_llm_model(&self, model: &str) -> Result<&Self, String>;
async fn define(&self, word: &str) -> Result<WordDefinition, String>;
// fn get_llm_models(&self) -> Result<Vec<String>, String>;
fn default_prompt(&self, word: &str) -> String {
PROMPT_TEMPLATE.replace("{:word}", word.trim())
}
}

async fn make_request(url: &str, body: serde_json::Value) -> Result<serde_json::Value, String> {
let req_client = reqwest::Client::new();
let res = req_client
.post(url)
.json(&body)
.send()
.await
.map_err(|e| format!("request error: {}", e))?;
if res.status() != StatusCode::OK {
return Err(format!("request error: {}", res.status()));
}
let json = res
.json::<serde_json::Value>()
#[wasm_bindgen]
pub async fn define_word(word: &str, api_key: &str) -> Result<JsValue, JsValue> {
let dictionary = GeminiDictionary::new(api_key);
let word_defination = dictionary
.define(word)
.await
.map_err(|e| format!("json error: {}", e))?;
Ok(json)
}
.map_err(|e| JsValue::from_str(&e))?;

fn extract_dictionary(res_body: serde_json::Value) -> Result<Word, String> {
let Some(dictionary_str) = res_body["candidates"][0]["content"]["parts"][0]["text"].as_str()
else {
return Err("json error: dictionary not found".to_string());
};
let dictionary_str = dictionary_str
.trim_start_matches("```json")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
let dictionary =
serde_json::from_str::<Word>(dictionary_str).map_err(|e| format!("serde error: {}", e))?;
Ok(dictionary)
Ok(serde_wasm_bindgen::to_value(&word_defination)?)
}
2 changes: 1 addition & 1 deletion plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "logseq-plugin-vocabulary-card",
"version": "0.1.0",
"version": "0.1.1",
"description": "A logseq plugin for creating vocabulary card",
"main": "dist/index.html",
"scripts": {
Expand Down