initial commit]
This commit is contained in:
178
_kiku_back.html
Normal file
178
_kiku_back.html
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<div id="SentenceTranslation"></div>
|
||||||
|
<!-- Kiku Note v1.4.0
|
||||||
|
This file is auto-generated. Any manual changes will be lost on save.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- biome-ignore format: this looks nicer-->
|
||||||
|
<div
|
||||||
|
id="kiku-root"
|
||||||
|
data-theme="__DATA_THEME__"
|
||||||
|
data-blur-nsfw="__DATA_BLUR_NSFW__"
|
||||||
|
data-mod-vertical="__DATA_MOD_VERTICAL__"
|
||||||
|
>
|
||||||
|
<!-- Do not modify anything if you don't know what you're doing Σ(°ロ°) -->
|
||||||
|
<div data-hk="00000000000000000" class="overflow-y-auto overflow-x-hidden gutter-stable h-svh font-primary transition-colors relative"><!--$--><!--/--><div class="flex flex-col gap-6 p-2 sm:p-4 bg-base-100 min-h-full max-w-4xl mx-auto pt-10 sm:pt-14"><!--!$--><div data-hk="000000000000000034" class="flex flex-col gap-4"><div class="flex rounded-lg gap-4 flex-col sm:flex-row "><div class="flex-1 bg-base-200 p-4 rounded-lg flex flex-col items-center justify-center sm:min-h-56"><div class="expression font-secondary text-center vertical-rl">{{#ExpressionFurigana}}{{furigana:ExpressionFurigana}}{{/ExpressionFurigana}}{{^ExpressionFurigana}}{{Expression}}{{/ExpressionFurigana}}</div><div class="mt-6 flex gap-4 pitch pitch-field " data-has-pitch="{{#PitchPosition}}true{{/PitchPosition}}">{{#PitchPosition}}<span> </span>{{/PitchPosition}}</div><div class="hidden sm:block sm:h-8 sm:mt-2"></div></div><!--$--><div data-hk="0000000000000000350" class="sm:max-w-1/2 bg-base-200 flex sm:items-center rounded-lg relative overflow-hidden justify-center picture-field-container " data-has-picture="{{#Picture}}true{{/Picture}}"><div class="picture-field-background">{{Picture}}</div><div class="picture-field " data-tags="{{Tags}}" data-nsfw="false" >{{Picture}}</div></div><!--/--></div><!--$--><!--/--></div><!--!$--></div><!--$--><!--/--></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- biome-ignore format: $-->
|
||||||
|
<div>
|
||||||
|
<link ref="preload" href="_kiku_font_hina-mincho.woff2" as="font" type="font/woff2" >
|
||||||
|
<link ref="preload" href="_kiku_font_klee-one.woff2" as="font" type="font/woff2" >
|
||||||
|
<link ref="preload" href="_kiku_font_ibm-plex-sans-jp.woff2" as="font" type="font/woff2" >
|
||||||
|
<link rel="stylesheet" href="_kiku.css" id="kiku-css">
|
||||||
|
<link href="_kiku.js" data-anki-include>
|
||||||
|
<link href="_kiku_libs.js" data-anki-include>
|
||||||
|
<link href="_kiku_shared.js" data-anki-include>
|
||||||
|
<link href="_kiku_lazy.js" data-anki-include>
|
||||||
|
<link href="_kiku_worker.js" data-anki-include>
|
||||||
|
<link href="_kiku_plugin.js" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_from_keisei.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_lookup.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_manual.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_old_script.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_stroke_edit_dist.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_wk_niai_noto.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_db_similar_kanji_yl_radical.json.gz" data-anki-include>
|
||||||
|
<link href="_kiku_back.html" data-anki-include>
|
||||||
|
<link href="_kiku_front.html" data-anki-include>
|
||||||
|
<link href="_kiku_style.css" data-anki-include>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- biome-ignore format: this looks nicer -->
|
||||||
|
<div style="display: none" id="anki-fields">
|
||||||
|
<div data-field="Expression">{{Expression}}</div>
|
||||||
|
<div data-field="ExpressionFurigana">{{ExpressionFurigana}}</div>
|
||||||
|
<div data-field="ExpressionReading">{{ExpressionReading}}</div>
|
||||||
|
<div data-field="ExpressionAudio">{{ExpressionAudio}}</div>
|
||||||
|
<div data-field="SelectionText">{{SelectionText}}</div>
|
||||||
|
<div data-field="MainDefinition">{{MainDefinition}}</div>
|
||||||
|
<div data-field="DefinitionPicture">{{DefinitionPicture}}</div>
|
||||||
|
<div data-field="Sentence">{{Sentence}}</div>
|
||||||
|
<div data-field="SentenceFurigana">{{SentenceFurigana}}</div>
|
||||||
|
<div data-field="SentenceAudio">{{SentenceAudio}}</div>
|
||||||
|
<div data-field="Picture">{{Picture}}</div>
|
||||||
|
<div data-field="Glossary">{{Glossary}}</div>
|
||||||
|
<div data-field="Hint">{{Hint}}</div>
|
||||||
|
<div data-field="IsWordAndSentenceCard">{{IsWordAndSentenceCard}}</div>
|
||||||
|
<div data-field="IsClickCard">{{IsClickCard}}</div>
|
||||||
|
<div data-field="IsSentenceCard">{{IsSentenceCard}}</div>
|
||||||
|
<div data-field="IsAudioCard">{{IsAudioCard}}</div>
|
||||||
|
<div data-field="PitchPosition">{{PitchPosition}}</div>
|
||||||
|
<div data-field="PitchCategories">{{PitchCategories}}</div>
|
||||||
|
<div data-field="Frequency">{{Frequency}}</div>
|
||||||
|
<div data-field="FreqSort">{{FreqSort}}</div>
|
||||||
|
<div data-field="MiscInfo">{{MiscInfo}}</div>
|
||||||
|
<div data-field="Tags">{{Tags}}</div>
|
||||||
|
<div data-field="furigana:ExpressionFurigana">{{furigana:ExpressionFurigana}}</div>
|
||||||
|
<div data-field="kana:ExpressionFurigana">{{kana:ExpressionFurigana}}</div>
|
||||||
|
<div data-field="furigana:Sentence">{{furigana:Sentence}}</div>
|
||||||
|
<div data-field="kanji:Sentence">{{kanji:Sentence}}</div>
|
||||||
|
<div data-field="furigana:SentenceFurigana">{{furigana:SentenceFurigana}}</div>
|
||||||
|
<div data-field="kana:SentenceFurigana">{{kana:SentenceFurigana}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>window._$HY||(e=>{let t=e=>e&&e.hasAttribute&&(e.hasAttribute("data-hk")?e:t(e.host&&e.host.nodeType?e.host:e.parentNode));["click", "input"].forEach((o=>document.addEventListener(o,(o=>{if(!e.events)return;let s=t(o.composedPath&&o.composedPath()[0]||o.target);s&&!e.completed.has(s)&&e.events.push([s,o])}))))})(_$HY={events:[],completed:new WeakSet,r:{},fe(){}});</script><!--xs-->
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { init } from "./_kiku.js";
|
||||||
|
|
||||||
|
init({ side: "back", ssr: true });
|
||||||
|
</script>
|
||||||
170
_kiku_plugin.js
Normal file
170
_kiku_plugin.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* @import { KikuPlugin } from "#/plugins/pluginTypes";
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { KikuPlugin }
|
||||||
|
*/
|
||||||
|
export const plugin = {
|
||||||
|
Sentence: (props) => {
|
||||||
|
const h = props.ctx.h;
|
||||||
|
|
||||||
|
function getPlainTextFromHtml(html) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.innerHTML = html ?? "";
|
||||||
|
return (el.textContent ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRouterTranslate({ sentenceText, targetLang }) {
|
||||||
|
const apiKey = "OPENROUTER_API_KEY";
|
||||||
|
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "openai/gpt-oss-120b:free",
|
||||||
|
temperature: 0.2,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content:
|
||||||
|
"You are a translation engine. Translate faithfully and naturally. Return only the translation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Translate the following sentence into ${targetLang}:\n\n${sentenceText}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg =
|
||||||
|
data?.error?.message ||
|
||||||
|
data?.message ||
|
||||||
|
`OpenRouter request failed (${res.status})`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = data?.choices?.[0]?.message?.content;
|
||||||
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
|
throw new Error("OpenRouter returned an empty response");
|
||||||
|
}
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSpinner() {
|
||||||
|
return h("span", {
|
||||||
|
class:
|
||||||
|
"loading loading-spinner loading-xs text-base-content-faint align-middle",
|
||||||
|
"aria-label": "Loading",
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
function SentenceTranslation() {
|
||||||
|
const sentenceHtml =
|
||||||
|
"Sentence" in (props.ctx.ankiFields ?? {})
|
||||||
|
? props.ctx.ankiFields?.Sentence
|
||||||
|
: document.getElementById("Sentence")?.innerHTML;
|
||||||
|
|
||||||
|
const sentenceText = getPlainTextFromHtml(sentenceHtml);
|
||||||
|
if (!sentenceText) return null;
|
||||||
|
|
||||||
|
const translationHtml =
|
||||||
|
"SentenceTranslation" in (props.ctx.ankiFields ?? {})
|
||||||
|
? props.ctx.ankiFields?.SentenceTranslation
|
||||||
|
: document.getElementById("SentenceTranslation")?.innerHTML;
|
||||||
|
|
||||||
|
// Treat placeholder as empty
|
||||||
|
const existingTranslationText = getPlainTextFromHtml(translationHtml);
|
||||||
|
const hasExistingTranslation =
|
||||||
|
!!existingTranslationText &&
|
||||||
|
existingTranslationText !== "Click to translate";
|
||||||
|
|
||||||
|
const targetLang =
|
||||||
|
(
|
||||||
|
localStorage.getItem("translation_target_lang") ||
|
||||||
|
sessionStorage.getItem("translation_target_lang") ||
|
||||||
|
"English"
|
||||||
|
)?.trim() || "English";
|
||||||
|
|
||||||
|
const root = h("div", {
|
||||||
|
class: "SentenceTranslation mt-2",
|
||||||
|
})();
|
||||||
|
|
||||||
|
const output = h("div", {
|
||||||
|
class: "text-lg text-base-content-calm whitespace-pre-wrap",
|
||||||
|
})();
|
||||||
|
|
||||||
|
const button = h("button", {
|
||||||
|
class: "btn btn-sm btn-ghost mt-1",
|
||||||
|
type: "button",
|
||||||
|
innerHTML: "Translate",
|
||||||
|
})();
|
||||||
|
|
||||||
|
const status = h("div", {
|
||||||
|
class: "text-xs text-base-content-faint mt-1 min-h-4",
|
||||||
|
})();
|
||||||
|
|
||||||
|
const fieldDiv = document.getElementById("SentenceTranslation");
|
||||||
|
|
||||||
|
let isLoading = false;
|
||||||
|
const spinner = createSpinner();
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
button.disabled = true;
|
||||||
|
status.textContent = "";
|
||||||
|
output.textContent = "";
|
||||||
|
status.appendChild(spinner);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const translationText = await openRouterTranslate({
|
||||||
|
sentenceText,
|
||||||
|
targetLang,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display result
|
||||||
|
output.textContent = translationText;
|
||||||
|
|
||||||
|
// Also update the field div if it exists on the back template
|
||||||
|
if (fieldDiv) fieldDiv.textContent = translationText;
|
||||||
|
} catch (e) {
|
||||||
|
const msg =
|
||||||
|
e instanceof Error ? e.message : "Translation failed unexpectedly";
|
||||||
|
status.textContent = msg;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
button.disabled = false;
|
||||||
|
if (spinner.parentNode) spinner.parentNode.removeChild(spinner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("click", run);
|
||||||
|
|
||||||
|
if (hasExistingTranslation) {
|
||||||
|
output.innerHTML = translationHtml;
|
||||||
|
root.appendChild(output);
|
||||||
|
} else {
|
||||||
|
root.appendChild(button);
|
||||||
|
root.appendChild(status);
|
||||||
|
root.appendChild(output);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can inline the CSS here
|
||||||
|
const style = h(
|
||||||
|
"style",
|
||||||
|
`
|
||||||
|
.SentenceTranslation .btn[disabled] { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [props.DefaultSentence(), SentenceTranslation(), style()];
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user