b3-dict 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- package/dist/app.d.ts +15 -0
- package/dist/app.js +95 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +114 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/answer-view.d.ts +6 -0
- package/dist/components/answer-view.js +27 -0
- package/dist/components/answer-view.js.map +1 -0
- package/dist/components/banner.d.ts +5 -0
- package/dist/components/banner.js +12 -0
- package/dist/components/banner.js.map +1 -0
- package/dist/components/cursor.d.ts +3 -0
- package/dist/components/cursor.js +6 -0
- package/dist/components/cursor.js.map +1 -0
- package/dist/components/gutter-line.d.ts +7 -0
- package/dist/components/gutter-line.js +12 -0
- package/dist/components/gutter-line.js.map +1 -0
- package/dist/components/lookup-list.d.ts +12 -0
- package/dist/components/lookup-list.js +23 -0
- package/dist/components/lookup-list.js.map +1 -0
- package/dist/components/message-block.d.ts +3 -0
- package/dist/components/message-block.js +11 -0
- package/dist/components/message-block.js.map +1 -0
- package/dist/components/query-input.d.ts +7 -0
- package/dist/components/query-input.js +21 -0
- package/dist/components/query-input.js.map +1 -0
- package/dist/lib/args.d.ts +15 -0
- package/dist/lib/args.js +41 -0
- package/dist/lib/args.js.map +1 -0
- package/dist/lib/ask.d.ts +4 -0
- package/dist/lib/ask.js +54 -0
- package/dist/lib/ask.js.map +1 -0
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/db.d.ts +27 -0
- package/dist/lib/db.js +98 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/envelope.d.ts +37 -0
- package/dist/lib/envelope.js +74 -0
- package/dist/lib/envelope.js.map +1 -0
- package/dist/lib/lookup.d.ts +25 -0
- package/dist/lib/lookup.js +41 -0
- package/dist/lib/lookup.js.map +1 -0
- package/dist/lib/messages.d.ts +90 -0
- package/dist/lib/messages.js +192 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/model.d.ts +176 -0
- package/dist/lib/model.js +249 -0
- package/dist/lib/model.js.map +1 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +23 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/policy.d.ts +22 -0
- package/dist/lib/policy.js +92 -0
- package/dist/lib/policy.js.map +1 -0
- package/dist/lib/render-text.d.ts +2 -0
- package/dist/lib/render-text.js +28 -0
- package/dist/lib/render-text.js.map +1 -0
- package/dist/lib/settings.d.ts +9 -0
- package/dist/lib/settings.js +29 -0
- package/dist/lib/settings.js.map +1 -0
- package/dist/lib/theme.d.ts +14 -0
- package/dist/lib/theme.js +16 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/time.d.ts +1 -0
- package/dist/lib/time.js +23 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/lib/types.d.ts +47 -0
- package/dist/lib/types.js +7 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/screens/help.d.ts +4 -0
- package/dist/screens/help.js +12 -0
- package/dist/screens/help.js.map +1 -0
- package/dist/screens/home.d.ts +14 -0
- package/dist/screens/home.js +45 -0
- package/dist/screens/home.js.map +1 -0
- package/dist/screens/list.d.ts +9 -0
- package/dist/screens/list.js +13 -0
- package/dist/screens/list.js.map +1 -0
- package/dist/screens/model-picker.d.ts +9 -0
- package/dist/screens/model-picker.js +20 -0
- package/dist/screens/model-picker.js.map +1 -0
- package/dist/screens/setup.d.ts +5 -0
- package/dist/screens/setup.js +11 -0
- package/dist/screens/setup.js.map +1 -0
- package/package.json +48 -4
package/dist/lib/ask.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
3
|
+
import { generateText, Output } from 'ai';
|
|
4
|
+
import { AnswerEnvelope } from './envelope.js';
|
|
5
|
+
import { classify } from './policy.js';
|
|
6
|
+
import { err, ok } from './types.js';
|
|
7
|
+
// Room for a three-sense meaning in Vietnamese; the exemplars run 200–300 tokens.
|
|
8
|
+
export const MAX_OUTPUT_TOKENS = 2500;
|
|
9
|
+
const TIMEOUT_MS = 60_000;
|
|
10
|
+
// Prompt rules validated by the answer-shape prototype (27 calls, 6 models).
|
|
11
|
+
const SYSTEM = [
|
|
12
|
+
'You are a dictionary assistant for a Vietnamese developer.',
|
|
13
|
+
'The user asks freeform questions (often mixed English/Vietnamese) about English words, phrases, idioms, or usage.',
|
|
14
|
+
'Classify the question into exactly one kind:',
|
|
15
|
+
'- word-check: asking whether something is a real English word/term, or what the correct word is.',
|
|
16
|
+
'- meaning: asking what a word/phrase/idiom means.',
|
|
17
|
+
'- interpretation: asking how to understand a specific passage, joke, or sentence in context.',
|
|
18
|
+
'All *Vi fields must be in Vietnamese. Keep quoted English inside examples/nuances as-is. Be concise; dictionary register, not chatty.',
|
|
19
|
+
'Rules: suggestions is empty when the word IS valid. synonyms must never include the term itself. subject must quote the interpreted text verbatim. Fields not used by the chosen kind are null.',
|
|
20
|
+
].join('\n');
|
|
21
|
+
function modelFor({ route, model, apiKey }) {
|
|
22
|
+
if (route === 'openai')
|
|
23
|
+
return createOpenAI({ apiKey })(model);
|
|
24
|
+
const openrouter = createOpenRouter({ apiKey });
|
|
25
|
+
// OpenAI's endpoints on OpenRouter refuse to run with reasoning off (HTTP 400), and their models
|
|
26
|
+
// already fit the output cap, so they get no reasoning option.
|
|
27
|
+
if (model.startsWith('openai/'))
|
|
28
|
+
return openrouter(model);
|
|
29
|
+
// A dictionary lookup doesn't need the model to think first. Reasoning models like glm-4.6 spend
|
|
30
|
+
// the whole output budget on reasoning and never get to the envelope; non-reasoning models ignore this.
|
|
31
|
+
return openrouter(model, { reasoning: { enabled: false, effort: 'none' } });
|
|
32
|
+
}
|
|
33
|
+
// One model call, no retries of its own: retry policy lives in policy.ts.
|
|
34
|
+
export function createAsk(config) {
|
|
35
|
+
const model = modelFor(config);
|
|
36
|
+
return async (query) => {
|
|
37
|
+
try {
|
|
38
|
+
const { output } = await generateText({
|
|
39
|
+
model,
|
|
40
|
+
system: SYSTEM,
|
|
41
|
+
prompt: query,
|
|
42
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
43
|
+
maxRetries: 0,
|
|
44
|
+
abortSignal: AbortSignal.timeout(TIMEOUT_MS),
|
|
45
|
+
output: Output.object({ schema: AnswerEnvelope }),
|
|
46
|
+
});
|
|
47
|
+
return ok(output);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return err(classify(e));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask.js","sourceRoot":"","sources":["../../src/lib/ask.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,YAAY,CAAC;AAElD,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AACtC,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,6EAA6E;AAC7E,MAAM,MAAM,GAAG;IACb,4DAA4D;IAC5D,mHAAmH;IACnH,8CAA8C;IAC9C,kGAAkG;IAClG,mDAAmD;IACnD,8FAA8F;IAC9F,uIAAuI;IACvI,iMAAiM;CAClM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,SAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAU;IAChD,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAChD,iGAAiG;IACjG,+DAA+D;IAC/D,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1D,iGAAiG;IACjG,wGAAwG;IACxG,OAAO,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE;QACrB,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,YAAY,CAAC;gBACpC,KAAK;gBACL,MAAM,EAAE,MAAM;gBACd,MAAM,EAAE,KAAK;gBACb,eAAe,EAAE,iBAAiB;gBAClC,UAAU,EAAE,CAAC;gBACb,WAAW,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;gBAC5C,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;aAClD,CAAC,CAAC;YACH,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Config, type ConfigProblem, type Result, type Route } from './types.js';
|
|
2
|
+
export declare const DEFAULT_MODEL: Record<Route, string>;
|
|
3
|
+
export declare const MODEL_CHOICES: Record<Route, readonly string[]>;
|
|
4
|
+
type Env = Record<string, string | undefined>;
|
|
5
|
+
export declare function checkModel(route: Route, id: string): Result<string, ConfigProblem>;
|
|
6
|
+
export declare function resolveConfig(env: Env, modelFlag?: string, saved?: string): Result<Config, ConfigProblem>;
|
|
7
|
+
export declare const keyVarFor: (route: Route) => string;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { err, ok, ROUTES } from './types.js';
|
|
2
|
+
export const DEFAULT_MODEL = {
|
|
3
|
+
openrouter: 'qwen/qwen3-235b-a22b-2507',
|
|
4
|
+
openai: 'gpt-5-mini',
|
|
5
|
+
};
|
|
6
|
+
// What the TUI's model picker offers, default first. Every one has answered a real lookup.
|
|
7
|
+
export const MODEL_CHOICES = {
|
|
8
|
+
openrouter: [DEFAULT_MODEL.openrouter, 'qwen/qwen3-30b-a3b-instruct-2507', 'z-ai/glm-4.6', 'openai/gpt-5-mini'],
|
|
9
|
+
openai: [DEFAULT_MODEL.openai],
|
|
10
|
+
};
|
|
11
|
+
const KEY_VAR = {
|
|
12
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
13
|
+
openai: 'OPENAI_API_KEY',
|
|
14
|
+
};
|
|
15
|
+
const read = (env, name) => env[name]?.trim() || undefined;
|
|
16
|
+
const isRoute = (value) => ROUTES.includes(value);
|
|
17
|
+
// A model id as the route will serve it. A slash id is an OpenRouter id; a bare name resolves on
|
|
18
|
+
// whichever route is active, so on OpenRouter it means OpenAI's model of that name.
|
|
19
|
+
export function checkModel(route, id) {
|
|
20
|
+
const model = id.trim();
|
|
21
|
+
if (model.includes('/')) {
|
|
22
|
+
return route === 'openrouter' ? ok(model) : err({ code: 'model-needs-openrouter', model });
|
|
23
|
+
}
|
|
24
|
+
return ok(route === 'openrouter' ? `openai/${model}` : model);
|
|
25
|
+
}
|
|
26
|
+
// Which model wins: the one-shot --model, then B3_DICT_MODEL, then the model picked in the TUI
|
|
27
|
+
// (saved), then the route's default. A saved model the route can't serve is skipped, not an error:
|
|
28
|
+
// the user didn't ask for it on this run.
|
|
29
|
+
export function resolveConfig(env, modelFlag, saved) {
|
|
30
|
+
const keys = {
|
|
31
|
+
openrouter: read(env, KEY_VAR.openrouter),
|
|
32
|
+
openai: read(env, KEY_VAR.openai),
|
|
33
|
+
};
|
|
34
|
+
if (!keys.openrouter && !keys.openai)
|
|
35
|
+
return err({ code: 'no-key' });
|
|
36
|
+
const forced = read(env, 'B3_DICT_PROVIDER')?.toLowerCase();
|
|
37
|
+
if (forced !== undefined && !isRoute(forced))
|
|
38
|
+
return err({ code: 'bad-provider', value: forced });
|
|
39
|
+
const route = forced ?? (keys.openrouter ? 'openrouter' : 'openai');
|
|
40
|
+
const apiKey = keys[route];
|
|
41
|
+
if (!apiKey)
|
|
42
|
+
return err({ code: 'route-key-missing', route });
|
|
43
|
+
const requested = modelFlag?.trim() || read(env, 'B3_DICT_MODEL');
|
|
44
|
+
if (requested) {
|
|
45
|
+
const model = checkModel(route, requested);
|
|
46
|
+
return model.ok ? ok({ route, apiKey, model: model.data }) : model;
|
|
47
|
+
}
|
|
48
|
+
const remembered = saved?.trim() ? checkModel(route, saved) : undefined;
|
|
49
|
+
return ok({ route, apiKey, model: remembered?.ok ? remembered.data : DEFAULT_MODEL[route] });
|
|
50
|
+
}
|
|
51
|
+
export const keyVarFor = (route) => KEY_VAR[route];
|
|
52
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/db.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { Kind } from './envelope.js';
|
|
3
|
+
import { type Result, type Route } from './types.js';
|
|
4
|
+
export type Db = Database.Database;
|
|
5
|
+
export type LookupRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
query_text: string;
|
|
8
|
+
kind: Kind;
|
|
9
|
+
answer_json: string;
|
|
10
|
+
route: Route;
|
|
11
|
+
model: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
saved: boolean;
|
|
14
|
+
saved_at: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type NewLookup = Omit<LookupRow, 'saved' | 'saved_at'>;
|
|
17
|
+
export type ListRow = Omit<LookupRow, 'answer_json' | 'route' | 'model'>;
|
|
18
|
+
export type ListKind = 'history' | 'wordbook';
|
|
19
|
+
export type Counts = Record<ListKind, number>;
|
|
20
|
+
export declare const normalizeQuery: (text: string) => string;
|
|
21
|
+
export declare function openDb(path: string): Result<Db>;
|
|
22
|
+
export declare function findCached(db: Db, queryText: string): Result<LookupRow | undefined>;
|
|
23
|
+
export declare function insertLookup(db: Db, row: NewLookup): Result<LookupRow>;
|
|
24
|
+
export declare function setSaved(db: Db, id: string, saved: boolean, now: Date): Result<LookupRow>;
|
|
25
|
+
export declare function listLookups(db: Db, which: ListKind): Result<ListRow[]>;
|
|
26
|
+
export declare function countLookups(db: Db): Result<Counts>;
|
|
27
|
+
export declare function getLookup(db: Db, id: string): Result<LookupRow | undefined>;
|
package/dist/lib/db.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { err, messageOf, ok } from './types.js';
|
|
5
|
+
const fromRaw = (raw) => ({ ...raw, saved: raw.saved === 1 });
|
|
6
|
+
const LIST_SQL = {
|
|
7
|
+
history: 'SELECT id, query_text, kind, created_at, saved, saved_at FROM lookups ORDER BY created_at DESC, rowid DESC',
|
|
8
|
+
wordbook: 'SELECT id, query_text, kind, created_at, saved, saved_at FROM lookups WHERE saved = 1 ORDER BY saved_at DESC, rowid DESC',
|
|
9
|
+
};
|
|
10
|
+
const SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS lookups (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
query_text TEXT NOT NULL,
|
|
14
|
+
kind TEXT NOT NULL,
|
|
15
|
+
answer_json TEXT NOT NULL,
|
|
16
|
+
route TEXT NOT NULL,
|
|
17
|
+
model TEXT NOT NULL,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
saved INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
saved_at TEXT NULL
|
|
21
|
+
)
|
|
22
|
+
`;
|
|
23
|
+
// The cache key: same words, whatever the spacing or line breaks. Case still counts.
|
|
24
|
+
export const normalizeQuery = (text) => text.trim().replace(/\s+/g, ' ');
|
|
25
|
+
export function openDb(path) {
|
|
26
|
+
try {
|
|
27
|
+
if (path !== ':memory:')
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
const db = new Database(path);
|
|
30
|
+
db.pragma('journal_mode = WAL');
|
|
31
|
+
db.function('norm_ws', { deterministic: true }, (text) => normalizeQuery(String(text)));
|
|
32
|
+
db.exec(SCHEMA);
|
|
33
|
+
return ok(db);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return err(messageOf(e));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function findCached(db, queryText) {
|
|
40
|
+
try {
|
|
41
|
+
const row = db
|
|
42
|
+
.prepare('SELECT * FROM lookups WHERE norm_ws(query_text) = ? ORDER BY created_at DESC, rowid DESC LIMIT 1')
|
|
43
|
+
.get(normalizeQuery(queryText));
|
|
44
|
+
return ok(row && fromRaw(row));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return err(messageOf(e));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function insertLookup(db, row) {
|
|
51
|
+
try {
|
|
52
|
+
db.prepare(`INSERT INTO lookups (id, query_text, kind, answer_json, route, model, created_at)
|
|
53
|
+
VALUES (@id, @query_text, @kind, @answer_json, @route, @model, @created_at)`).run(row);
|
|
54
|
+
return ok({ ...row, saved: false, saved_at: null });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return err(messageOf(e));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// The wordbook is this flag: a saved lookup is a wordbook entry.
|
|
61
|
+
export function setSaved(db, id, saved, now) {
|
|
62
|
+
try {
|
|
63
|
+
const row = db
|
|
64
|
+
.prepare('UPDATE lookups SET saved = ?, saved_at = ? WHERE id = ? RETURNING *')
|
|
65
|
+
.get(saved ? 1 : 0, saved ? now.toISOString() : null, id);
|
|
66
|
+
return row ? ok(fromRaw(row)) : err(`no lookup with id ${id}`);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return err(messageOf(e));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function listLookups(db, which) {
|
|
73
|
+
try {
|
|
74
|
+
return ok(db.prepare(LIST_SQL[which]).all().map(fromRaw));
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return err(messageOf(e));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function countLookups(db) {
|
|
81
|
+
try {
|
|
82
|
+
const row = db.prepare('SELECT COUNT(*) AS history, COALESCE(SUM(saved), 0) AS wordbook FROM lookups').get();
|
|
83
|
+
return ok({ history: row.history, wordbook: row.wordbook });
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return err(messageOf(e));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function getLookup(db, id) {
|
|
90
|
+
try {
|
|
91
|
+
const row = db.prepare('SELECT * FROM lookups WHERE id = ?').get(id);
|
|
92
|
+
return ok(row && fromRaw(row));
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
return err(messageOf(e));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type Result } from './types.js';
|
|
3
|
+
export declare const KINDS: readonly ["word-check", "meaning", "interpretation"];
|
|
4
|
+
export type Kind = (typeof KINDS)[number];
|
|
5
|
+
export declare const AnswerEnvelope: z.ZodObject<{
|
|
6
|
+
kind: z.ZodEnum<{
|
|
7
|
+
"word-check": "word-check";
|
|
8
|
+
meaning: "meaning";
|
|
9
|
+
interpretation: "interpretation";
|
|
10
|
+
}>;
|
|
11
|
+
term: z.ZodNullable<z.ZodString>;
|
|
12
|
+
isWord: z.ZodNullable<z.ZodBoolean>;
|
|
13
|
+
verdictVi: z.ZodNullable<z.ZodString>;
|
|
14
|
+
suggestions: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
15
|
+
term: z.ZodString;
|
|
16
|
+
reasonVi: z.ZodString;
|
|
17
|
+
}, z.core.$strip>>>;
|
|
18
|
+
ipa: z.ZodNullable<z.ZodString>;
|
|
19
|
+
entries: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
20
|
+
partOfSpeech: z.ZodString;
|
|
21
|
+
glossVi: z.ZodString;
|
|
22
|
+
register: z.ZodNullable<z.ZodString>;
|
|
23
|
+
examples: z.ZodArray<z.ZodObject<{
|
|
24
|
+
text: z.ZodString;
|
|
25
|
+
glossVi: z.ZodString;
|
|
26
|
+
}, z.core.$strip>>;
|
|
27
|
+
synonyms: z.ZodArray<z.ZodString>;
|
|
28
|
+
}, z.core.$strip>>>;
|
|
29
|
+
subject: z.ZodNullable<z.ZodString>;
|
|
30
|
+
explanationVi: z.ZodNullable<z.ZodString>;
|
|
31
|
+
nuances: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type Answer = z.infer<typeof AnswerEnvelope>;
|
|
34
|
+
export declare function checkContent(a: Answer): string | null;
|
|
35
|
+
export declare const displayTerm: (answer: Answer, query: string) => string;
|
|
36
|
+
export declare const bareIpa: (ipa: string) => string;
|
|
37
|
+
export declare function parseStored(answerJson: string): Result<Answer>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { err, messageOf, ok } from './types.js';
|
|
3
|
+
export const KINDS = ['word-check', 'meaning', 'interpretation'];
|
|
4
|
+
// Flat on purpose: every field present, null when the kind doesn't use it.
|
|
5
|
+
// A discriminated union breaks OpenAI strict structured outputs (top-level oneOf).
|
|
6
|
+
export const AnswerEnvelope = z.object({
|
|
7
|
+
kind: z.enum(KINDS),
|
|
8
|
+
// word-check
|
|
9
|
+
term: z.string().nullable(),
|
|
10
|
+
isWord: z.boolean().nullable(),
|
|
11
|
+
verdictVi: z.string().nullable(),
|
|
12
|
+
suggestions: z
|
|
13
|
+
.array(z.object({
|
|
14
|
+
term: z.string(),
|
|
15
|
+
reasonVi: z.string().describe('why this is the right word, in Vietnamese'),
|
|
16
|
+
}))
|
|
17
|
+
.max(3)
|
|
18
|
+
.nullable(),
|
|
19
|
+
// meaning
|
|
20
|
+
ipa: z.string().nullable(),
|
|
21
|
+
entries: z
|
|
22
|
+
.array(z.object({
|
|
23
|
+
partOfSpeech: z.string(),
|
|
24
|
+
glossVi: z.string().describe('Vietnamese meaning'),
|
|
25
|
+
register: z.string().nullable().describe('formal / slang / idiom / technical…'),
|
|
26
|
+
examples: z
|
|
27
|
+
.array(z.object({
|
|
28
|
+
text: z.string().describe('English example sentence'),
|
|
29
|
+
glossVi: z.string().describe('Vietnamese gloss of the example'),
|
|
30
|
+
}))
|
|
31
|
+
.max(2),
|
|
32
|
+
synonyms: z.array(z.string()).max(4),
|
|
33
|
+
}))
|
|
34
|
+
.max(3)
|
|
35
|
+
.nullable(),
|
|
36
|
+
// interpretation
|
|
37
|
+
subject: z.string().nullable().describe('the text being interpreted, verbatim'),
|
|
38
|
+
explanationVi: z.string().nullable().describe('Vietnamese explanation of meaning/intent'),
|
|
39
|
+
nuances: z.array(z.string()).max(3).nullable().describe('wordplay, tone, register notes'),
|
|
40
|
+
});
|
|
41
|
+
const blank = (s) => s === null || s.trim() === '';
|
|
42
|
+
// An envelope can pass the schema and still say nothing. Returns what's missing, or null.
|
|
43
|
+
export function checkContent(a) {
|
|
44
|
+
switch (a.kind) {
|
|
45
|
+
case 'word-check':
|
|
46
|
+
if (a.isWord === null)
|
|
47
|
+
return 'word-check without isWord';
|
|
48
|
+
if (blank(a.verdictVi))
|
|
49
|
+
return 'word-check without verdictVi';
|
|
50
|
+
return null;
|
|
51
|
+
case 'meaning':
|
|
52
|
+
if (!a.entries?.some((e) => !blank(e.glossVi)))
|
|
53
|
+
return 'meaning without any glossVi';
|
|
54
|
+
return null;
|
|
55
|
+
case 'interpretation':
|
|
56
|
+
if (blank(a.explanationVi))
|
|
57
|
+
return 'interpretation without explanationVi';
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// The headline term: what the model named, or the question itself.
|
|
62
|
+
export const displayTerm = (answer, query) => answer.term?.trim() || query.trim();
|
|
63
|
+
// Models send IPA with or without slashes; renderers add their own.
|
|
64
|
+
export const bareIpa = (ipa) => ipa.replace(/^\/|\/$/g, '');
|
|
65
|
+
export function parseStored(answerJson) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = AnswerEnvelope.safeParse(JSON.parse(answerJson));
|
|
68
|
+
return parsed.success ? ok(parsed.data) : err(parsed.error.message);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return err(messageOf(e));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/lib/envelope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Db, type LookupRow } from './db.js';
|
|
2
|
+
import { type Answer } from './envelope.js';
|
|
3
|
+
import { type RetryEvent } from './policy.js';
|
|
4
|
+
import { type Failure, type Result, type Route } from './types.js';
|
|
5
|
+
export type Ask = (query: string) => Promise<Result<Answer, Failure>>;
|
|
6
|
+
export type LookupDeps = {
|
|
7
|
+
db: Db;
|
|
8
|
+
ask: Ask;
|
|
9
|
+
sleep: (ms: number) => Promise<void>;
|
|
10
|
+
now: () => Date;
|
|
11
|
+
newId: () => string;
|
|
12
|
+
onRetry?: (event: RetryEvent) => void;
|
|
13
|
+
};
|
|
14
|
+
export type LookupRequest = {
|
|
15
|
+
query: string;
|
|
16
|
+
route: Route;
|
|
17
|
+
model: string;
|
|
18
|
+
fresh: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type LookupOutcome = {
|
|
21
|
+
answer: Answer;
|
|
22
|
+
entry: LookupRow;
|
|
23
|
+
cached: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function lookup(request: LookupRequest, deps: LookupDeps): Promise<Result<LookupOutcome, Failure>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { findCached, insertLookup } from './db.js';
|
|
2
|
+
import { checkContent, parseStored } from './envelope.js';
|
|
3
|
+
import { runWithPolicy } from './policy.js';
|
|
4
|
+
import { err, ok } from './types.js';
|
|
5
|
+
// One lookup, start to finish: reuse a stored answer, or ask the model and store what comes back.
|
|
6
|
+
// Failures write nothing, so history and the cache only ever hold answers that render.
|
|
7
|
+
export async function lookup(request, deps) {
|
|
8
|
+
const { query, route, model, fresh } = request;
|
|
9
|
+
if (!fresh) {
|
|
10
|
+
const cached = findCached(deps.db, query);
|
|
11
|
+
if (!cached.ok)
|
|
12
|
+
return err({ reason: 'unexpected', detail: cached.error });
|
|
13
|
+
if (cached.data) {
|
|
14
|
+
const stored = parseStored(cached.data.answer_json);
|
|
15
|
+
if (stored.ok)
|
|
16
|
+
return ok({ answer: stored.data, entry: cached.data, cached: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const asked = await runWithPolicy(async () => {
|
|
20
|
+
const result = await deps.ask(query);
|
|
21
|
+
if (!result.ok)
|
|
22
|
+
return result;
|
|
23
|
+
const missing = checkContent(result.data);
|
|
24
|
+
return missing ? err({ reason: 'bad-output', detail: `empty answer (${missing})` }) : result;
|
|
25
|
+
}, deps);
|
|
26
|
+
if (!asked.ok)
|
|
27
|
+
return asked;
|
|
28
|
+
const inserted = insertLookup(deps.db, {
|
|
29
|
+
id: deps.newId(),
|
|
30
|
+
query_text: query,
|
|
31
|
+
kind: asked.data.kind,
|
|
32
|
+
answer_json: JSON.stringify(asked.data),
|
|
33
|
+
route,
|
|
34
|
+
model,
|
|
35
|
+
created_at: deps.now().toISOString(),
|
|
36
|
+
});
|
|
37
|
+
if (!inserted.ok)
|
|
38
|
+
return err({ reason: 'unexpected', detail: inserted.error });
|
|
39
|
+
return ok({ answer: asked.data, entry: inserted.data, cached: false });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.js","sourceRoot":"","sources":["../../src/lib/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAA2B,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,WAAW,EAAe,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,aAAa,EAAmB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAyC,MAAM,YAAY,CAAC;AAiB5E,kGAAkG;AAClG,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAAsB,EAAE,IAAgB;IACnE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE/C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,GAAG,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3E,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACpD,IAAI,MAAM,CAAC,EAAE;gBAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,KAAK,IAAI,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,MAAM,CAAC;QAC9B,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1C,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxG,CAAC,EAAE,IAAI,CAAC,CAAC;IACT,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IAE5B,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE;QACrC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE;QAChB,UAAU,EAAE,KAAK;QACjB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI;QACrB,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;QACvC,KAAK;QACL,KAAK;QACL,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAE/E,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Origin } from './model.js';
|
|
2
|
+
import type { RetryEvent } from './policy.js';
|
|
3
|
+
import type { Config, ConfigProblem, Failure, Route } from './types.js';
|
|
4
|
+
export declare const KEY_URL: Record<Route, string>;
|
|
5
|
+
export declare const HELP = "B3 Dict \u2014 h\u1ECFi v\u1EC1 t\u1EEB, th\u00E0nh ng\u1EEF, c\u00E1ch d\u00F9ng ti\u1EBFng Anh; tr\u1EA3 l\u1EDDi b\u1EB1ng ti\u1EBFng Vi\u1EC7t.\n\nC\u00E1ch d\u00F9ng:\n b3dict m\u1EDF giao di\u1EC7n h\u1ECFi \u0111\u00E1p\n b3dict \"<c\u00E2u h\u1ECFi>\" [tu\u1EF3 ch\u1ECDn] h\u1ECFi m\u1ED9t l\u1EA7n r\u1ED3i tho\u00E1t\n\nV\u00ED d\u1EE5:\n b3dict \"take for granted\"\n b3dict \"Is draggable an English word?\"\n b3dict \"pragmatic\" --model z-ai/glm-4.6\n\nTu\u1EF3 ch\u1ECDn:\n --fresh b\u1ECF qua \u0111\u00E1p \u00E1n \u0111\u00E3 l\u01B0u, h\u1ECFi l\u1EA1i model\n --model <id> d\u00F9ng model n\u00E0y cho m\u1ED9t l\u1EA7n h\u1ECFi (lu\u00F4n h\u1ECFi l\u1EA1i model)\n --json in nguy\u00EAn answer envelope d\u1EA1ng JSON\n -h, --help hi\u1EC7n tr\u1EE3 gi\u00FAp\n -V, --version hi\u1EC7n phi\u00EAn b\u1EA3n\n\nM\u00E3 tho\u00E1t:\n 0 th\u00E0nh c\u00F4ng \u00B7 1 l\u1ED7i kh\u00F4ng r\u00F5 \u00B7 2 l\u1ED7i c\u00E1ch d\u00F9ng\n 3 l\u1ED7i c\u1EA5u h\u00ECnh (key, provider, model) \u00B7 4 l\u1ED7i provider";
|
|
6
|
+
export declare const CLI: {
|
|
7
|
+
readonly unknownOption: (name: string) => string;
|
|
8
|
+
readonly badArguments: (detail: string) => string;
|
|
9
|
+
readonly missingQuery: "Thiếu câu hỏi. Ví dụ: b3dict \"take for granted\"";
|
|
10
|
+
readonly missingModel: "--model cần một tên model. Ví dụ: --model z-ai/glm-4.6";
|
|
11
|
+
readonly dbOpenFailed: (path: string, detail: string) => string;
|
|
12
|
+
readonly unknownError: (detail: string) => string;
|
|
13
|
+
readonly settingsIgnored: (path: string, detail: string) => string;
|
|
14
|
+
};
|
|
15
|
+
export declare const backTo: (origin?: Origin) => string;
|
|
16
|
+
export declare const TUI: {
|
|
17
|
+
readonly subtitle: "AI-first dictionary";
|
|
18
|
+
readonly needsTerminal: "Giao diện b3dict cần chạy trong terminal. Muốn hỏi một lần: b3dict \"<câu hỏi>\"";
|
|
19
|
+
readonly setupHeading: "Setup";
|
|
20
|
+
readonly homeHeading: "Home";
|
|
21
|
+
readonly countFailed: (detail: string) => string;
|
|
22
|
+
readonly homeInstruction: "Hỏi về từ, thành ngữ, cách dùng. Trộn tiếng Việt và English đều được.";
|
|
23
|
+
readonly homeHintsEmpty: "gõ câu hỏi rồi enter · ↓ menu · esc thoát";
|
|
24
|
+
readonly homeHints: "enter hỏi · ↓ menu · esc xoá ô · ctrl+c thoát";
|
|
25
|
+
readonly homeMenuHints: "↑↓ chọn · enter mở · esc về ô gõ · q thoát";
|
|
26
|
+
readonly homeMenuTitle: "── hoặc chọn: ──";
|
|
27
|
+
readonly menuLabel: {
|
|
28
|
+
readonly history: "History ";
|
|
29
|
+
readonly wordbook: "Wordbook";
|
|
30
|
+
readonly model: "Model ";
|
|
31
|
+
readonly help: "Help ";
|
|
32
|
+
};
|
|
33
|
+
readonly listHeading: {
|
|
34
|
+
readonly history: "History";
|
|
35
|
+
readonly wordbook: "Wordbook";
|
|
36
|
+
};
|
|
37
|
+
readonly listCount: {
|
|
38
|
+
readonly history: (n: number) => string;
|
|
39
|
+
readonly wordbook: (n: number) => string;
|
|
40
|
+
};
|
|
41
|
+
readonly listEmpty: {
|
|
42
|
+
readonly history: "Chưa có lookup nào. Về home và gõ một câu hỏi.";
|
|
43
|
+
readonly wordbook: "Chưa lưu từ nào. Bấm s trên một đáp án để lưu ★.";
|
|
44
|
+
};
|
|
45
|
+
readonly listHints: {
|
|
46
|
+
readonly history: "↑↓ di chuyển · enter mở · s lưu/bỏ ★ · esc về home · q thoát";
|
|
47
|
+
readonly wordbook: "↑↓ di chuyển · enter mở · s bỏ lưu · esc về home · q thoát";
|
|
48
|
+
};
|
|
49
|
+
readonly listEmptyHints: "esc về home · q thoát";
|
|
50
|
+
readonly listFailed: (detail: string) => string;
|
|
51
|
+
readonly moreAbove: (n: number) => string;
|
|
52
|
+
readonly moreBelow: (n: number) => string;
|
|
53
|
+
readonly savedAgo: (age: string) => string;
|
|
54
|
+
readonly asking: (model: string) => string;
|
|
55
|
+
readonly askingHints: (backTo: string) => string;
|
|
56
|
+
readonly cached: "đáp án đã lưu từ trước";
|
|
57
|
+
readonly answerHints: (saved: boolean, backTo: string) => string;
|
|
58
|
+
readonly saveFailed: (detail: string) => string;
|
|
59
|
+
readonly failedHints: (backTo: string) => string;
|
|
60
|
+
readonly setupHints: "esc thoát";
|
|
61
|
+
readonly modelHeading: "Model";
|
|
62
|
+
readonly modelInstruction: "Model trả lời các câu hỏi sau. Đáp án đã lưu vẫn dùng lại; r để hỏi lại bằng model mới.";
|
|
63
|
+
readonly modelDefault: "mặc định";
|
|
64
|
+
readonly modelNotes: Record<string, string>;
|
|
65
|
+
readonly modelInUse: "✓ đang dùng";
|
|
66
|
+
readonly customRow: "Khác: gõ id model…";
|
|
67
|
+
readonly customExample: "Id trên OpenRouter, ví dụ z-ai/glm-4.6. Tên trần như gpt-5-nano là model của OpenAI.";
|
|
68
|
+
readonly customRejected: (model: string) => string;
|
|
69
|
+
readonly pinned: (model: string) => string;
|
|
70
|
+
readonly pickerHints: "↑↓ chọn · enter dùng · esc về home · q thoát";
|
|
71
|
+
readonly customHints: "enter dùng · esc huỷ";
|
|
72
|
+
readonly settingsSaveFailed: (detail: string) => string;
|
|
73
|
+
readonly helpHeading: "Help";
|
|
74
|
+
readonly helpHints: "esc về home · q thoát";
|
|
75
|
+
readonly isWord: "✓ từ có thật";
|
|
76
|
+
readonly notWord: "✗ không phải từ chuẩn";
|
|
77
|
+
};
|
|
78
|
+
export type HelpPaths = {
|
|
79
|
+
env: string;
|
|
80
|
+
settings: string;
|
|
81
|
+
db: string;
|
|
82
|
+
};
|
|
83
|
+
export type HelpSection = {
|
|
84
|
+
title: string;
|
|
85
|
+
rows: [string, string][];
|
|
86
|
+
};
|
|
87
|
+
export declare const helpSections: (paths: HelpPaths) => HelpSection[];
|
|
88
|
+
export declare function configProblemText(problem: ConfigProblem, envPath: string): string;
|
|
89
|
+
export declare function failureText(failure: Failure, config: Config, query: string): string;
|
|
90
|
+
export declare function retryText(event: RetryEvent): string;
|