lesson 23 LLM sentence explainer
This commit is contained in:
1 parent
075fe19382
commit
e734d09039
10 files changed
+744
-25
No files matched your search
@@ -66,6 +66,15 @@
|
||||
</div>
|
||||
<div v-else v-html="v.notes"></div>
|
||||
</blockquote>
|
||||
<div v-if="example">
|
||||
<blockquote
|
||||
v-if="exNotes"
|
||||
v-html="markdownIt.render(exNotes)"
|
||||
></blockquote>
|
||||
<button type="button" v-else @click="explainSentence(example.ko)">
|
||||
Explain sentence
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
<details :class="$style.options">
|
||||
@@ -125,6 +134,8 @@ import yaml from "yaml";
|
||||
import SentenceAudio from "./SentenceAudio.vue";
|
||||
import { supabase } from "../supabase.ts";
|
||||
import { IVoc, Voc } from "../types.ts";
|
||||
import { LLMstream } from "../llm.ts";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
const { fmKey = "vocabulary", category } = defineProps<{
|
||||
fmKey?: string;
|
||||
@@ -140,6 +151,8 @@ const exIndex = ref(0);
|
||||
const isEdit = ref(false);
|
||||
const vEditContent = ref("");
|
||||
|
||||
const exNotes = ref("");
|
||||
|
||||
const quizState = reactive({
|
||||
vChosenCats: [] as string[],
|
||||
isAutoplayAudio: true,
|
||||
@@ -275,6 +288,8 @@ function makeNewExIndex() {
|
||||
}
|
||||
|
||||
function mark(isRight: boolean) {
|
||||
exNotes.value = "";
|
||||
|
||||
nextTick(() => {
|
||||
if (sentenceDetails.value) {
|
||||
sentenceDetails.value.open = false;
|
||||
@@ -378,6 +393,32 @@ async function onVContentSave() {
|
||||
isEdit.value = false;
|
||||
}
|
||||
|
||||
const markdownIt = new MarkdownIt({
|
||||
html: false,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
});
|
||||
|
||||
async function explainSentence(s: string) {
|
||||
for await (const data of LLMstream({
|
||||
model: "deepseek-v4-flash",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"Explain how this sentence works in 500 characters. Give key vocabularies.",
|
||||
},
|
||||
{ role: "user", content: s },
|
||||
],
|
||||
})) {
|
||||
if (data.done) return;
|
||||
if (data.content) {
|
||||
exNotes.value += data.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onModalClickOutside(ev: MouseEvent) {
|
||||
if (!(ev.target instanceof HTMLElement)) return;
|
||||
if (ev.target.matches('[name="edit"]')) return;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
//@ts-ignore
|
||||
const { VITE_LLM_API_KEY } = import.meta.env;
|
||||
|
||||
// Minimal type definitions
|
||||
type DeepSeekChunk = {
|
||||
choices: Array<{
|
||||
delta: {
|
||||
content?: string;
|
||||
reasoning_content?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
// Type guard - simple and effective
|
||||
function isValidChunk(data: unknown): data is DeepSeekChunk {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
data !== null &&
|
||||
"choices" in data &&
|
||||
Array.isArray((data as any).choices) &&
|
||||
(data as any).choices.length > 0 &&
|
||||
"delta" in (data as any).choices[0]
|
||||
);
|
||||
}
|
||||
|
||||
// Helper - one-liner safe parse
|
||||
function safeParse<T>(text: string): T | null {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function* LLMstream(body: {
|
||||
model: string;
|
||||
messages: {
|
||||
role: "system" | "user";
|
||||
content: string;
|
||||
}[];
|
||||
}) {
|
||||
if (!VITE_LLM_API_KEY) return;
|
||||
|
||||
let endpoint = "https://api.deepseek.com/chat/completions";
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${VITE_LLM_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += value;
|
||||
|
||||
const events = buffer.split("\n\n");
|
||||
buffer = events.pop() || "";
|
||||
|
||||
for (const event of events) {
|
||||
for (const line of event.split("\n")) {
|
||||
const DATA_START = "data: ";
|
||||
|
||||
if (!line.startsWith(DATA_START)) continue;
|
||||
const payload = line.slice(DATA_START.length);
|
||||
|
||||
if (payload === "[DONE]") {
|
||||
yield { content: "", done: true };
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = safeParse<DeepSeekChunk>(payload);
|
||||
|
||||
if (!chunk?.choices?.[0]?.delta) continue;
|
||||
|
||||
const delta = chunk.choices[0].delta;
|
||||
|
||||
// Yield content if present
|
||||
if (delta.content) {
|
||||
yield {
|
||||
content: delta.content,
|
||||
reasoning: delta.reasoning_content,
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Yield reasoning separately if present and no content
|
||||
if (delta.reasoning_content && !delta.content) {
|
||||
yield {
|
||||
content: "",
|
||||
reasoning: delta.reasoning_content,
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user