| import { ParseOptions, PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| import { StandardBoxData, TranslationResult } from '../providers/types.js'; | ||
| /** | ||
| * Base class for all dictionary-style prompt strategies. | ||
| * Provides shared parsing and rendering logic for dictionary entries. | ||
| * Subclasses only need to override `id` and `getSystemMessage()`. | ||
| */ | ||
| export declare abstract class BaseDictionaryStrategy implements PromptStrategy { | ||
| abstract id: string; | ||
| protected createRawResult(): TranslationResult; | ||
| createStreamParser(options?: ParseOptions): { | ||
| feed: (chunk: string) => void; | ||
| getResult: () => StandardBoxData; | ||
| }; | ||
| getInitialState(): StandardBoxData; | ||
| abstract getSystemMessage(targetLang: string): string; | ||
| parse(text: string, options?: ParseOptions): StandardBoxData; | ||
| protected toStandardBoxData(tr: TranslationResult, targetLang: string): StandardBoxData; | ||
| } |
| import { TagStreamParser } from '../utils/tag-parser.js'; | ||
| /** | ||
| * Base class for all dictionary-style prompt strategies. | ||
| * Provides shared parsing and rendering logic for dictionary entries. | ||
| * Subclasses only need to override `id` and `getSystemMessage()`. | ||
| */ | ||
| export class BaseDictionaryStrategy { | ||
| createRawResult() { | ||
| return { | ||
| antonyms: [], | ||
| definitions: [], | ||
| examples: [], | ||
| idioms: [], | ||
| items: [], | ||
| meaning: [], | ||
| phonetic: '', | ||
| synonyms: [], | ||
| tags: [], | ||
| word: '' | ||
| }; | ||
| } | ||
| createStreamParser(options) { | ||
| const targetLang = options?.targetLang || 'zh'; | ||
| const raw = this.createRawResult(); | ||
| let currentDefinition = null; | ||
| const addItem = (type, value) => { | ||
| raw.items = raw.items || []; | ||
| raw.items.push({ type, value }); | ||
| }; | ||
| const parser = new TagStreamParser((event) => { | ||
| switch (event.type) { | ||
| case 'block_end': { | ||
| currentDefinition = null; | ||
| break; | ||
| } | ||
| case 'block_start': { | ||
| if (event.name === 'DEF') { | ||
| currentDefinition = { examples: [], meaning: '', partOfSpeech: '' }; | ||
| raw.definitions.push(currentDefinition); | ||
| addItem('definition', currentDefinition); | ||
| } | ||
| break; | ||
| } | ||
| case 'tag': { | ||
| switch (event.key) { | ||
| case 'ANTONYM': { | ||
| raw.antonyms.push(event.value); | ||
| addItem('antonym', event.value); | ||
| break; | ||
| } | ||
| case 'EXAMPLE': { | ||
| if (currentDefinition) { | ||
| currentDefinition.examples.push(event.value); | ||
| } | ||
| raw.examples.push(event.value); | ||
| break; | ||
| } | ||
| case 'IDIOM': { | ||
| raw.idioms.push(event.value); | ||
| addItem('idiom', event.value); | ||
| break; | ||
| } | ||
| case 'MEANING': { | ||
| if (currentDefinition) { | ||
| currentDefinition.meaning = event.value; | ||
| } | ||
| raw.meaning.push(event.value); | ||
| break; | ||
| } | ||
| case 'ORIGIN': { | ||
| raw.origin = event.value; | ||
| addItem('origin', event.value); | ||
| break; | ||
| } | ||
| case 'PHONETIC': { | ||
| raw.phonetic = event.value; | ||
| break; | ||
| } | ||
| case 'POS': { | ||
| if (currentDefinition) { | ||
| currentDefinition.partOfSpeech = event.value; | ||
| } | ||
| else { | ||
| currentDefinition = { examples: [], meaning: '', partOfSpeech: event.value }; | ||
| raw.definitions.push(currentDefinition); | ||
| addItem('definition', currentDefinition); | ||
| } | ||
| break; | ||
| } | ||
| case 'SYNONYM': { | ||
| raw.synonyms.push(event.value); | ||
| addItem('synonym', event.value); | ||
| break; | ||
| } | ||
| case 'TAG': { | ||
| raw.tags.push(event.value); | ||
| break; | ||
| } | ||
| case 'WORD': { | ||
| raw.word = event.value; | ||
| break; | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| // No default | ||
| } | ||
| }); | ||
| return { | ||
| feed: (chunk) => parser.feed(chunk), | ||
| getResult: () => this.toStandardBoxData(raw, targetLang) | ||
| }; | ||
| } | ||
| getInitialState() { | ||
| return { | ||
| content: [], | ||
| tags: [], | ||
| title: '' | ||
| }; | ||
| } | ||
| parse(text, options) { | ||
| const parserWrapper = this.createStreamParser(options); | ||
| parserWrapper.feed(text); | ||
| return parserWrapper.getResult(); | ||
| } | ||
| toStandardBoxData(tr, targetLang) { | ||
| const sections = []; | ||
| const isChinese = targetLang.toLowerCase().startsWith('zh'); | ||
| const synonymsLabel = isChinese ? '同义词' : 'Synonyms'; | ||
| const antonymsLabel = isChinese ? '反义词' : 'Antonyms'; | ||
| const originLabel = isChinese ? '词源' : 'Origin'; | ||
| const idiomsLabel = isChinese ? '成语' : 'Idioms'; | ||
| const examplesLabel = isChinese ? '示例' : 'Examples'; | ||
| // Use ordered items if available (preserves LLM output order) | ||
| if (tr.items && tr.items.length > 0) { | ||
| let lastSection = null; | ||
| let lastType = null; | ||
| for (const item of tr.items) { | ||
| switch (item.type) { | ||
| case 'antonym': { | ||
| if (lastType === 'antonym' && lastSection) { | ||
| const prevContent = lastSection.content[0]; | ||
| prevContent.text += `, ${item.value}`; | ||
| } | ||
| else { | ||
| const section = { content: [{ style: 'dim', text: `${antonymsLabel}: ${item.value}` }] }; | ||
| sections.push(section); | ||
| lastSection = section; | ||
| lastType = 'antonym'; | ||
| } | ||
| break; | ||
| } | ||
| case 'definition': { | ||
| const def = item.value; | ||
| const content = [{ style: 'green', text: ` • ${def.meaning}` }]; | ||
| if (def.examples && def.examples.length > 0) { | ||
| for (const ex of def.examples) { | ||
| content.push({ style: 'dim', text: ` - ${ex}` }); | ||
| } | ||
| } | ||
| const section = { | ||
| content, | ||
| title: { style: 'cyan-bold', text: def.partOfSpeech }, | ||
| }; | ||
| sections.push(section); | ||
| lastSection = section; | ||
| lastType = 'definition'; | ||
| break; | ||
| } | ||
| case 'idiom': { | ||
| if (lastType === 'idiom' && lastSection) { | ||
| lastSection.content.push({ style: 'dim', text: `• ${item.value}` }); | ||
| } | ||
| else { | ||
| const section = { | ||
| content: [{ style: 'dim', text: `• ${item.value}` }], | ||
| title: { style: 'magenta-bold', text: `${idiomsLabel}:` } | ||
| }; | ||
| sections.push(section); | ||
| lastSection = section; | ||
| lastType = 'idiom'; | ||
| } | ||
| break; | ||
| } | ||
| case 'origin': { | ||
| if (lastType === 'origin' && lastSection) { | ||
| const prevContent = lastSection.content[0]; | ||
| prevContent.text += ` ${item.value}`; | ||
| } | ||
| else { | ||
| const section = { content: [{ style: 'dim', text: `${originLabel}: ${item.value}` }] }; | ||
| sections.push(section); | ||
| lastSection = section; | ||
| lastType = 'origin'; | ||
| } | ||
| break; | ||
| } | ||
| case 'synonym': { | ||
| if (lastType === 'synonym' && lastSection) { | ||
| const prevContent = lastSection.content[0]; | ||
| prevContent.text += `, ${item.value}`; | ||
| } | ||
| else { | ||
| const section = { content: [{ style: 'dim', text: `${synonymsLabel}: ${item.value}` }] }; | ||
| sections.push(section); | ||
| lastSection = section; | ||
| lastType = 'synonym'; | ||
| } | ||
| break; | ||
| } | ||
| // No default | ||
| } | ||
| } | ||
| return { | ||
| content: sections, | ||
| subtitle: tr.phonetic, | ||
| tags: tr.tags, | ||
| title: tr.word, | ||
| }; | ||
| } | ||
| // Fallback: Legacy rendering (grouped by type) | ||
| if (tr.definitions && tr.definitions.length > 0) { | ||
| for (const def of tr.definitions) { | ||
| const content = [{ style: 'green', text: ` • ${def.meaning}` }]; | ||
| if (def.examples && def.examples.length > 0) { | ||
| for (const ex of def.examples) { | ||
| content.push({ style: 'dim', text: ` - ${ex}` }); | ||
| } | ||
| } | ||
| sections.push({ | ||
| content, | ||
| title: { style: 'cyan-bold', text: def.partOfSpeech }, | ||
| }); | ||
| } | ||
| } | ||
| else if (tr.meaning && tr.meaning.length > 0) { | ||
| const content = tr.meaning.map(m => ({ style: 'green', text: `• ${m}` })); | ||
| if (tr.examples && tr.examples.length > 0) { | ||
| content.push('', { style: 'yellow', text: `${examplesLabel}:` }); | ||
| for (const ex of tr.examples) { | ||
| content.push({ style: 'dim', text: `• ${ex}` }); | ||
| } | ||
| } | ||
| sections.push({ content }); | ||
| } | ||
| if (tr.idioms && tr.idioms.length > 0) { | ||
| const content = tr.idioms.map(i => ({ style: 'dim', text: `• ${i}` })); | ||
| sections.push({ | ||
| content, | ||
| title: { style: 'magenta-bold', text: `${idiomsLabel}:` }, | ||
| }); | ||
| } | ||
| if (!tr.items || tr.items.length === 0) { | ||
| if (tr.synonyms && tr.synonyms.length > 0) { | ||
| sections.push({ content: [{ style: 'dim', text: `${synonymsLabel}: ${tr.synonyms.join(', ')}` }] }); | ||
| } | ||
| if (tr.antonyms && tr.antonyms.length > 0) { | ||
| sections.push({ content: [{ style: 'dim', text: `${antonymsLabel}: ${tr.antonyms.join(', ')}` }] }); | ||
| } | ||
| if (tr.origin) { | ||
| sections.push({ content: [{ style: 'dim', text: `${originLabel}: ${tr.origin}` }] }); | ||
| } | ||
| } | ||
| return { | ||
| content: sections, | ||
| subtitle: tr.phonetic, | ||
| tags: tr.tags, | ||
| title: tr.word, | ||
| }; | ||
| } | ||
| } |
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export declare class CambridgePromptStrategy extends BaseDictionaryStrategy { | ||
| id: string; | ||
| getSystemMessage(targetLang: string): string; | ||
| } |
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export class CambridgePromptStrategy extends BaseDictionaryStrategy { | ||
| id = 'cambridge'; | ||
| getSystemMessage(targetLang) { | ||
| return ` | ||
| You are simulating the Cambridge Dictionary, the world's leading learner's dictionary. | ||
| Provide a Cambridge-style entry for the user input. Translate into "${targetLang}". | ||
| Style Guidelines: | ||
| - CLEAR, SIMPLE definitions that language learners can understand easily. | ||
| - Show BOTH UK and US pronunciation when they differ. | ||
| - Include CEFR level (A1-C2) to indicate difficulty. | ||
| - Grammar patterns: show how the word is used in structures (e.g. [T], [I], [+ to infinitive]). | ||
| - Common mistakes or usage notes for learners. | ||
| - Examples should use simple, natural English. | ||
| CRITICAL INSTRUCTION: | ||
| - Response MUST be a stream of data lines using tags. | ||
| - DO NOT use JSON. | ||
| - STRICTLY FOLLOW THE STRUCTURE BELOW. | ||
| [WORD] The original source word ONLY | ||
| [PHONETIC] IPA: UK /.../ US /.../ (show both if different) | ||
| [TAG] CEFR level (A1, A2, B1, B2, C1, C2) and grammar info. One per line. | ||
| [BLOCK:DEF] | ||
| [POS] Part of Speech with grammar pattern (e.g. "verb [T]", "noun [C]", "noun [U]") | ||
| [MEANING] Simple, clear definition in target language. Use easy words to explain difficult ones. | ||
| [EXAMPLE] Natural example sentence with translation | ||
| [END] | ||
| [SYNONYM] Synonym with usage note - One per line | ||
| [IDIOM] Common phrase or expression - One per line | ||
| Example Output: | ||
| [WORD] hello | ||
| [PHONETIC] UK /heˈləʊ/ US /heˈloʊ/ | ||
| [TAG] A1 | ||
| [TAG] Greeting | ||
| [BLOCK:DEF] | ||
| [POS] exclamation | ||
| [MEANING] 你好,用于见面时的问候或打电话时的开头语 (used when you meet someone or start a phone conversation) | ||
| [EXAMPLE] Hello, how are you? 你好,你怎么样? | ||
| [END] | ||
| [BLOCK:DEF] | ||
| [POS] noun [C] | ||
| [MEANING] 问候,打招呼 (something you say or do to greet someone) | ||
| [EXAMPLE] I said hello but she didn't answer. 我打了声招呼但她没回应。 | ||
| [END] | ||
| [SYNONYM] hi - less formal | ||
| [SYNONYM] hey - very informal, used with friends | ||
| [IDIOM] say hello 打招呼 | ||
| `; | ||
| } | ||
| } |
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export declare class YoudaoPromptStrategy extends BaseDictionaryStrategy { | ||
| id: string; | ||
| getSystemMessage(targetLang: string): string; | ||
| } |
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export class YoudaoPromptStrategy extends BaseDictionaryStrategy { | ||
| id = 'youdao'; | ||
| getSystemMessage(targetLang) { | ||
| return ` | ||
| You are simulating Youdao Dictionary (有道词典), the most popular Chinese-English dictionary. | ||
| Provide a Youdao-style entry for the user input. Translate into "${targetLang}". | ||
| Style Guidelines: | ||
| - Prioritize PRACTICAL, everyday usage over academic precision. | ||
| - Show CONCISE Chinese translations first, then brief English explanations. | ||
| - Include internet slang, colloquial usage, and modern expressions where relevant. | ||
| - Examples should be short, practical sentences from daily life. | ||
| - Keep definitions compact: focus on the most common 2-3 meanings. | ||
| CRITICAL INSTRUCTION: | ||
| - Response MUST be a stream of data lines using tags. | ||
| - DO NOT use JSON. | ||
| - STRICTLY FOLLOW THE STRUCTURE BELOW. | ||
| [WORD] The original source word ONLY | ||
| [PHONETIC] IPA phonetic transcription | ||
| [TAG] Usage frequency or domain tags (e.g. "CET4", "Daily", "Internet", "Business"). One per line. | ||
| [BLOCK:DEF] | ||
| [POS] Part of Speech | ||
| [MEANING] Concise translation in target language (keep it short, like "你好;喂") | ||
| [EXAMPLE] Short practical example with translation | ||
| [END] | ||
| [SYNONYM] Synonym - One per line | ||
| [IDIOM] Common phrase or collocation - One per line | ||
| Example Output: | ||
| [WORD] hello | ||
| [PHONETIC] /hɛˈloʊ/ | ||
| [TAG] CET4 | ||
| [TAG] Daily | ||
| [BLOCK:DEF] | ||
| [POS] int. | ||
| [MEANING] 你好;喂(用于问候或打电话) | ||
| [EXAMPLE] Hello! How are you? 你好!你怎么样? | ||
| [END] | ||
| [BLOCK:DEF] | ||
| [POS] n. | ||
| [MEANING] 招呼;问候 | ||
| [EXAMPLE] She gave me a friendly hello. 她友好地跟我打了个招呼。 | ||
| [END] | ||
| [SYNONYM] hi - 口语常用 | ||
| [SYNONYM] hey - 非正式 | ||
| [IDIOM] say hello to 向…问好 | ||
| [IDIOM] hello world 你好世界(编程术语) | ||
| `; | ||
| } | ||
| } |
| export type TagEvent = { | ||
| content: string; | ||
| type: 'text'; | ||
| } | { | ||
| key: string; | ||
| type: 'tag'; | ||
| value: string; | ||
| } | { | ||
| name: string; | ||
| type: 'block_start'; | ||
| } | { | ||
| type: 'block_end'; | ||
| }; | ||
| export declare class TagStreamParser { | ||
| private onEvent; | ||
| private buffer; | ||
| constructor(onEvent: (event: TagEvent) => void); | ||
| feed(chunk: string): void; | ||
| private parseLine; | ||
| private process; | ||
| } |
| export class TagStreamParser { | ||
| onEvent; | ||
| buffer = ''; | ||
| constructor(onEvent) { | ||
| this.onEvent = onEvent; | ||
| } | ||
| feed(chunk) { | ||
| this.buffer += chunk; | ||
| this.process(); | ||
| } | ||
| parseLine(line) { | ||
| if (!line) | ||
| return; | ||
| // Match [TAG] Value or [TAG] | ||
| const match = line.match(/^\[([A-Z0-9_:-]+)\]\s*(.*)$/); | ||
| if (match) { | ||
| const tag = match[1]; | ||
| const value = match[2]; | ||
| if (tag.startsWith('BLOCK:')) { | ||
| this.onEvent({ name: tag.replace('BLOCK:', ''), type: 'block_start' }); | ||
| } | ||
| else if (tag === 'END') { | ||
| this.onEvent({ type: 'block_end' }); | ||
| } | ||
| else { | ||
| this.onEvent({ key: tag, type: 'tag', value }); | ||
| } | ||
| } | ||
| else { | ||
| // Treat as plain text content (maybe continuation of previous tag?) | ||
| // For strict protocol, we might ignore, but for robustness we emit text | ||
| this.onEvent({ content: line, type: 'text' }); | ||
| } | ||
| } | ||
| process() { | ||
| let newlineIndex; | ||
| while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) { | ||
| const line = this.buffer.slice(0, newlineIndex).trim(); | ||
| this.buffer = this.buffer.slice(newlineIndex + 1); | ||
| this.parseLine(line); | ||
| } | ||
| } | ||
| } |
@@ -45,4 +45,2 @@ import { confirm } from '@inquirer/prompts'; | ||
| const mode = configService.getMode(); | ||
| // Strategies (Hardcoded for now, but ready for config) | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const promptStrategy = PromptFactory.get(mode); | ||
@@ -59,3 +57,4 @@ const renderStrategy = RendererFactory.get('box'); | ||
| const stream = provider.stream(inputText, { systemMessage }); | ||
| let buffer = ''; | ||
| // Use streaming parser if available | ||
| const streamParser = promptStrategy.createStreamParser?.({ targetLang }); | ||
| let result = promptStrategy.getInitialState(); | ||
@@ -65,4 +64,10 @@ let lastRender = 0; | ||
| for await (const chunk of stream) { | ||
| buffer += chunk; | ||
| result = promptStrategy.parse(buffer); | ||
| if (streamParser) { | ||
| streamParser.feed(chunk); | ||
| result = streamParser.getResult(); | ||
| } | ||
| else { | ||
| // Fallback: accumulate and parse | ||
| result = promptStrategy.parse(chunk, { targetLang }); | ||
| } | ||
| const now = Date.now(); | ||
@@ -74,6 +79,5 @@ if (now - lastRender > 50) { | ||
| } | ||
| // Final Render | ||
| // Force override word with input text (Specific to Translation Strategy) | ||
| if (result && typeof result === 'object' && 'word' in result) { | ||
| result.word = inputText; | ||
| // Final result - override title with input text for word lookups | ||
| if (result.title === '' || !result.title) { | ||
| result.title = inputText; | ||
| } | ||
@@ -83,3 +87,3 @@ renderStrategy.render(result, false); | ||
| spinner.stop(); | ||
| // 2. Save History | ||
| // Save History | ||
| historyService.save(inputText, result, config.current, targetLang); | ||
@@ -86,0 +90,0 @@ } |
@@ -1,6 +0,14 @@ | ||
| export interface PromptStrategy<T = any> { | ||
| getInitialState(): T; | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export interface ParseOptions { | ||
| targetLang?: string; | ||
| } | ||
| export interface PromptStrategy { | ||
| createStreamParser?(options?: ParseOptions): { | ||
| feed(chunk: string): void; | ||
| getResult(): StandardBoxData; | ||
| }; | ||
| getInitialState(): StandardBoxData; | ||
| getSystemMessage(targetLang: string): string; | ||
| id: string; | ||
| parse(text: string): T; | ||
| parse(text: string, options?: ParseOptions): StandardBoxData; | ||
| } |
@@ -1,5 +0,6 @@ | ||
| export interface RenderStrategy<T = any> { | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export interface RenderStrategy { | ||
| id: string; | ||
| render(data: T, fromCache: boolean): void; | ||
| render(data: StandardBoxData, fromCache: boolean): void; | ||
| reset(): void; | ||
| } |
@@ -1,8 +0,13 @@ | ||
| import { PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| import { ParseOptions, PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export declare class GrammarPromptStrategy implements PromptStrategy<StandardBoxData> { | ||
| export declare class GrammarPromptStrategy implements PromptStrategy { | ||
| id: string; | ||
| createStreamParser(_options?: ParseOptions): { | ||
| feed: (chunk: string) => void; | ||
| getResult: () => StandardBoxData; | ||
| }; | ||
| getInitialState(): StandardBoxData; | ||
| getSystemMessage(_targetLang: string): string; | ||
| parse(text: string): StandardBoxData; | ||
| parse(text: string, options?: ParseOptions): StandardBoxData; | ||
| private toStandardBoxData; | ||
| } |
+42
-30
@@ -0,3 +1,33 @@ | ||
| import { TagStreamParser } from '../utils/tag-parser.js'; | ||
| export class GrammarPromptStrategy { | ||
| id = 'grammar'; | ||
| createStreamParser(_options) { | ||
| const raw = { | ||
| corrected: '', | ||
| improvements: [], | ||
| mistakes: [] | ||
| }; | ||
| const parser = new TagStreamParser((event) => { | ||
| if (event.type === 'tag') { | ||
| switch (event.key) { | ||
| case 'CORRECT': { | ||
| raw.corrected = event.value; | ||
| break; | ||
| } | ||
| case 'IMPROVE': { | ||
| raw.improvements.push(event.value); | ||
| break; | ||
| } | ||
| case 'MISTAKE': { | ||
| raw.mistakes.push(event.value); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| return { | ||
| feed: (chunk) => parser.feed(chunk), | ||
| getResult: () => this.toStandardBoxData(raw) | ||
| }; | ||
| } | ||
| getInitialState() { | ||
@@ -32,42 +62,24 @@ return { | ||
| } | ||
| parse(text) { | ||
| const lines = text.split('\n'); | ||
| // We recreate state each parse for simplicity, | ||
| // real streaming might want to accumulate content. | ||
| // For now, this is stateless parsing of the full accumulated buffer. | ||
| let corrected = ''; | ||
| const mistakes = []; | ||
| const improvements = []; | ||
| for (const line of lines) { | ||
| const clean = line.trim(); | ||
| if (!clean) | ||
| continue; | ||
| if (clean.startsWith('[CORRECT]')) { | ||
| corrected = clean.replace('[CORRECT]', '').trim(); | ||
| } | ||
| else if (clean.startsWith('[MISTAKE]')) { | ||
| mistakes.push(clean.replace('[MISTAKE]', '').trim()); | ||
| } | ||
| else if (clean.startsWith('[IMPROVE]')) { | ||
| improvements.push(clean.replace('[IMPROVE]', '').trim()); | ||
| } | ||
| } | ||
| parse(text, options) { | ||
| const parserWrapper = this.createStreamParser(options); | ||
| parserWrapper.feed(text); | ||
| return parserWrapper.getResult(); | ||
| } | ||
| toStandardBoxData(raw) { | ||
| const sections = []; | ||
| if (corrected) { | ||
| if (raw.corrected) { | ||
| sections.push({ | ||
| content: corrected, | ||
| style: 'code', | ||
| content: [{ style: 'code', text: raw.corrected }], | ||
| title: 'Correction' | ||
| }); | ||
| } | ||
| if (mistakes.length > 0) { | ||
| if (raw.mistakes.length > 0) { | ||
| sections.push({ | ||
| content: mistakes.map(m => `• ${m}`), | ||
| content: raw.mistakes.map(m => `• ${m}`), | ||
| title: 'Analysis' | ||
| }); | ||
| } | ||
| if (improvements.length > 0) { | ||
| if (raw.improvements.length > 0) { | ||
| sections.push({ | ||
| content: improvements.map(i => `• ${i}`), | ||
| style: 'dim', | ||
| content: raw.improvements.map(i => ({ style: 'dim', text: `• ${i}` })), | ||
| title: 'Suggestions' | ||
@@ -74,0 +86,0 @@ }); |
| import { PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| export declare const PromptFactory: { | ||
| get(id: string): PromptStrategy<any>; | ||
| register(strategy: PromptStrategy<any>): void; | ||
| get(id: string): PromptStrategy; | ||
| register(strategy: PromptStrategy): void; | ||
| }; |
@@ -0,12 +1,14 @@ | ||
| import { CambridgePromptStrategy } from './cambridge.js'; | ||
| import { GrammarPromptStrategy } from './grammar.js'; | ||
| import { OxfordPromptStrategy } from './oxford.js'; | ||
| import { TranslationPromptStrategy } from './translation.js'; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| import { YoudaoPromptStrategy } from './youdao.js'; | ||
| const strategies = { | ||
| 'cambridge': new CambridgePromptStrategy(), | ||
| 'grammar': new GrammarPromptStrategy(), | ||
| 'oxford': new OxfordPromptStrategy(), | ||
| 'translation': new TranslationPromptStrategy(), | ||
| 'youdao': new YoudaoPromptStrategy(), | ||
| }; | ||
| export const PromptFactory = { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| get(id) { | ||
@@ -19,3 +21,2 @@ const strategy = strategies[id]; | ||
| }, | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| register(strategy) { | ||
@@ -22,0 +23,0 @@ strategies[strategy.id] = strategy; |
@@ -1,8 +0,5 @@ | ||
| import { PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export declare class OxfordPromptStrategy implements PromptStrategy<StandardBoxData> { | ||
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export declare class OxfordPromptStrategy extends BaseDictionaryStrategy { | ||
| id: string; | ||
| getInitialState(): StandardBoxData; | ||
| getSystemMessage(_targetLang: string): string; | ||
| parse(text: string): StandardBoxData; | ||
| getSystemMessage(targetLang: string): string; | ||
| } |
+54
-81
@@ -1,88 +0,61 @@ | ||
| export class OxfordPromptStrategy { | ||
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export class OxfordPromptStrategy extends BaseDictionaryStrategy { | ||
| id = 'oxford'; | ||
| getInitialState() { | ||
| return { | ||
| content: [], | ||
| tags: ['Oxford'], | ||
| title: 'Oxford Dictionary', | ||
| }; | ||
| } | ||
| getSystemMessage(_targetLang) { | ||
| getSystemMessage(targetLang) { | ||
| return ` | ||
| You are the Oxford English Dictionary. Provide a comprehensive, academic definition of the word. | ||
| Focus on: British English pronunciation (IPA), precise definitions, etymology, and usage labels (e.g., formal, archaic). | ||
| You are simulating the Oxford English Dictionary (OED), the definitive historical dictionary of English. | ||
| Provide an Oxford-style scholarly entry for the user input. Translate into "${targetLang}". | ||
| Output Format (Stream of tags): | ||
| [WORD] The word | ||
| [PHONETIC] IPA (British) | ||
| [POS] Part of Speech (noun, verb, etc.) | ||
| [DEF] Definition (numbered) | ||
| [EXAMPLE] Usage example (italics preferred in rendering, but plain text here) | ||
| [ETYMOLOGY] Origin/History of the word | ||
| [LEVEL] CEFR Level (A1-C2) or Frequency label if applicable | ||
| Style Guidelines: | ||
| - FORMAL, AUTHORITATIVE, and PRECISE academic definitions. | ||
| - Prefer British English pronunciation and spelling. | ||
| - Include DETAILED etymology tracing the word's origin through history. | ||
| - Usage labels: formal, informal, archaic, literary, technical, dated, etc. | ||
| - Definitions ordered HISTORICALLY (earliest meaning first, then derived meanings). | ||
| - Examples should include literary or historical citations where possible. | ||
| Structure: | ||
| - Start with [WORD] and [PHONETIC]. | ||
| - Then [POS]. | ||
| - Under [POS], list [DEF] and [EXAMPLE] pairs. | ||
| - End with [ETYMOLOGY] and [LEVEL]. | ||
| CRITICAL INSTRUCTION: | ||
| - Response MUST be a stream of data lines using tags. | ||
| - DO NOT use JSON. | ||
| - STRICTLY FOLLOW THE STRUCTURE BELOW. | ||
| [WORD] The original source word ONLY | ||
| [PHONETIC] IPA phonetic (British RP preferred, e.g. /həˈləʊ/) | ||
| [TAG] Register and usage labels (e.g. "Formal", "British", "Literary", "Archaic"). One per line. | ||
| [BLOCK:DEF] | ||
| [POS] Part of Speech (with formal labels: n., v., adj., adv., int., etc.) | ||
| [MEANING] Scholarly definition in target language with English clarification in parentheses | ||
| [EXAMPLE] Literary or historical citation with source attribution if possible | ||
| [END] | ||
| [SYNONYM] Synonym with register distinction - One per line | ||
| [ANTONYM] Antonym with register distinction - One per line | ||
| [ORIGIN] DETAILED etymology: Proto-language roots → Old/Middle English evolution → modern usage. Include dates of first attestation. | ||
| Example Output: | ||
| [WORD] hello | ||
| [PHONETIC] /həˈləʊ/ | ||
| [TAG] Informal | ||
| [TAG] British | ||
| [BLOCK:DEF] | ||
| [POS] int. | ||
| [MEANING] 用以打招呼或引起注意 (Used as a greeting or to attract attention) | ||
| [EXAMPLE] "Hello, old fellow!" — P.G. Wodehouse, 1915 | ||
| [END] | ||
| [BLOCK:DEF] | ||
| [POS] n. | ||
| [MEANING] 招呼,问候之语 (An utterance of 'hello'; a greeting) | ||
| [EXAMPLE] She called out a cheerful hello. 她愉快地打了声招呼。 | ||
| [END] | ||
| [SYNONYM] salutation - Formal | ||
| [SYNONYM] greeting - Neutral | ||
| [ANTONYM] farewell - Formal | ||
| [ANTONYM] goodbye - Neutral | ||
| [ORIGIN] Mid 19th century (1827): variant of earlier hollo, hallo; related to Old High German halā, holā (emphatic imperative of holen 'to fetch'). First recorded use in American English. Popularized as telephone greeting by Thomas Edison (1877). | ||
| `; | ||
| } | ||
| parse(text) { | ||
| const lines = text.split('\n'); | ||
| const result = this.getInitialState(); | ||
| let currentPosSection = null; | ||
| let etymology = ''; | ||
| let level = ''; | ||
| // Temporary storage to build sections | ||
| const posSections = []; | ||
| for (const line of lines) { | ||
| const clean = line.trim(); | ||
| if (!clean) | ||
| continue; | ||
| if (clean.startsWith('[WORD]')) { | ||
| result.title = clean.replace('[WORD]', '').trim(); | ||
| } | ||
| else if (clean.startsWith('[PHONETIC]')) { | ||
| result.subtitle = clean.replace('[PHONETIC]', '').trim(); | ||
| } | ||
| else if (clean.startsWith('[LEVEL]')) { | ||
| level = clean.replace('[LEVEL]', '').trim(); | ||
| if (level) | ||
| result.tags?.push(level); | ||
| } | ||
| else if (clean.startsWith('[POS]')) { | ||
| const pos = clean.replace('[POS]', '').trim(); | ||
| currentPosSection = { content: [], title: pos }; | ||
| posSections.push(currentPosSection); | ||
| } | ||
| else if (clean.startsWith('[DEF]')) { | ||
| const def = clean.replace('[DEF]', '').trim(); | ||
| if (currentPosSection) { | ||
| currentPosSection.content.push(`• ${def}`); | ||
| } | ||
| } | ||
| else if (clean.startsWith('[EXAMPLE]')) { | ||
| const ex = clean.replace('[EXAMPLE]', '').trim(); | ||
| if (currentPosSection) { | ||
| // Add indentation for examples | ||
| currentPosSection.content.push(` "${ex}"`); | ||
| } | ||
| } | ||
| else if (clean.startsWith('[ETYMOLOGY]')) { | ||
| etymology = clean.replace('[ETYMOLOGY]', '').trim(); | ||
| } | ||
| } | ||
| // Assemble content | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| result.content = posSections; | ||
| if (etymology) { | ||
| result.content.push({ | ||
| content: [etymology], | ||
| style: 'dim', | ||
| title: 'Etymology' | ||
| }); | ||
| } | ||
| return result; | ||
| } | ||
| } |
@@ -1,8 +0,5 @@ | ||
| import { PromptStrategy } from '../interfaces/prompt-strategy.js'; | ||
| import { TranslationResult } from '../providers/types.js'; | ||
| export declare class TranslationPromptStrategy implements PromptStrategy<TranslationResult> { | ||
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export declare class TranslationPromptStrategy extends BaseDictionaryStrategy { | ||
| id: string; | ||
| getInitialState(): TranslationResult; | ||
| getSystemMessage(targetLang: string): string; | ||
| parse(text: string): TranslationResult; | ||
| } |
@@ -1,19 +0,8 @@ | ||
| export class TranslationPromptStrategy { | ||
| import { BaseDictionaryStrategy } from './base-dictionary.js'; | ||
| export class TranslationPromptStrategy extends BaseDictionaryStrategy { | ||
| id = 'translation'; | ||
| getInitialState() { | ||
| return { | ||
| antonyms: [], | ||
| definitions: [], | ||
| examples: [], | ||
| idioms: [], | ||
| meaning: [], | ||
| phonetic: '', | ||
| synonyms: [], | ||
| tags: [], | ||
| word: '' | ||
| }; | ||
| } | ||
| getSystemMessage(targetLang) { | ||
| return ` | ||
| You are a professional linguist and translation tool. Analyze the user input. | ||
| You are a Senior Lexicographer and professional translation engine. | ||
| Your task is to provide a comprehensive dictionary entry for the user input. | ||
| Translate the input into "${targetLang}". | ||
@@ -25,24 +14,25 @@ | ||
| - Format each field with a tag like [TAG] Content. | ||
| - Provide BILINGUAL content where appropriate. | ||
| - Provide BILINGUAL content where appropriate (Source + Target). | ||
| - STRICTLY FOLLOW THE STRUCTURE BELOW. | ||
| Required Tags: | ||
| [SCHEMA] LAP/1.0 | ||
| [MODE] translation | ||
| [WORD] The original source word ONLY | ||
| [PHONETIC] IPA phonetic transcription | ||
| [TAG] Domain tags (e.g. Computing, Medical, Slang) - One per line | ||
| [POS] Part of Speech (noun, verb, adj, etc.) - Starts a new definition block | ||
| [MEANING] Definition in target language (with source term in brackets if helpful) | ||
| [EXAMPLE] Example sentence - Must follow the relevant [POS] and [MEANING] | ||
| [SYNONYM] Synonyms - One per line | ||
| [ANTONYM] Antonyms - One per line | ||
| [IDIOM] Common idioms/phrases containing the word - One per line | ||
| [ORIGIN] Etymology/Origin of the word (brief) | ||
| [PHONETIC] IPA phonetic transcription (UK/US if different) | ||
| [TAG] Multi-dimensional tags: Field (e.g. Med), Register (e.g. Formal), Region (e.g. UK), Sentiment (e.g. Pejorative). One per line. | ||
| Structure: | ||
| - Always start with [WORD] and [PHONETIC]. | ||
| - Then [TAG]s. | ||
| - Then iterate through meanings, grouping them by [POS]. | ||
| - Inside each [POS], provide the [MEANING] followed immediately by relevant [EXAMPLE]s. | ||
| - End with [SYNONYM], [ANTONYM], [IDIOM], [ORIGIN]. | ||
| [BLOCK:DEF] | ||
| [POS] Part of Speech (noun, verb, adj, etc.) | ||
| [MEANING] Precise definition in target language. Order definitions by frequency. | ||
| [EXAMPLE] Bilingual example sentence showing common collocations. | ||
| [END] | ||
| [SYNONYM] Synonyms with nuance - One per line (e.g. "hi (嗨) - Informal") | ||
| [ANTONYM] Antonyms with nuance - One per line (e.g. "bad (坏) - General") | ||
| [IDIOM] Common idioms/phrases containing the word - One per line (e.g. "say hello to (向...问好)") | ||
| [ORIGIN] Rich etymology: Trace root -> development -> first use. (e.g. "From Old English hāl (whole) -> Middle English...") | ||
| Example Output: | ||
| [SCHEMA] LAP/1.0 | ||
| [MODE] translation | ||
| [WORD] hello | ||
@@ -52,67 +42,23 @@ [PHONETIC] /hɛˈloʊ/ | ||
| [TAG] Informal | ||
| [TAG] Interjection | ||
| [BLOCK:DEF] | ||
| [POS] interjection | ||
| [MEANING] 你好 (Used as a greeting or to begin a telephone conversation) | ||
| [EXAMPLE] Hello, Paul. (你好,保罗。) | ||
| [END] | ||
| [BLOCK:DEF] | ||
| [POS] noun | ||
| [MEANING] 招呼 (An utterance of 'hello'; a greeting) | ||
| [EXAMPLE] She said hello to me. (她向我打了个招呼。) | ||
| [SYNONYM] hi | ||
| [ANTONYM] goodbye | ||
| [END] | ||
| [SYNONYM] hi (嗨) - Casual | ||
| [SYNONYM] greetings (问候) - Formal | ||
| [ANTONYM] goodbye (再见) - General | ||
| [IDIOM] say hello to (向...问好) | ||
| [ORIGIN] Mid 19th century: alteration of hollo. | ||
| [ORIGIN] Mid 19th century: alteration of hollo; related to Old High German halā (hail). | ||
| `; | ||
| } | ||
| parse(text) { | ||
| const result = this.getInitialState(); | ||
| const lines = text.split('\n'); | ||
| let currentDefinition = null; | ||
| for (const line of lines) { | ||
| const cleanLine = line.trim(); | ||
| if (!cleanLine) | ||
| continue; | ||
| if (cleanLine.startsWith('[WORD]')) { | ||
| result.word = cleanLine.replace('[WORD]', '').trim(); | ||
| } | ||
| else if (cleanLine.startsWith('[PHONETIC]')) { | ||
| result.phonetic = cleanLine.replace('[PHONETIC]', '').trim(); | ||
| } | ||
| else if (cleanLine.startsWith('[POS]')) { | ||
| const pos = cleanLine.replace('[POS]', '').trim(); | ||
| currentDefinition = { examples: [], meaning: '', partOfSpeech: pos }; | ||
| result.definitions.push(currentDefinition); | ||
| } | ||
| else if (cleanLine.startsWith('[MEANING]')) { | ||
| const meaning = cleanLine.replace('[MEANING]', '').trim(); | ||
| if (currentDefinition) { | ||
| currentDefinition.meaning = meaning; | ||
| } | ||
| // Always add to flat list for compatibility | ||
| result.meaning.push(meaning); | ||
| } | ||
| else if (cleanLine.startsWith('[EXAMPLE]')) { | ||
| const example = cleanLine.replace('[EXAMPLE]', '').trim(); | ||
| if (currentDefinition) { | ||
| currentDefinition.examples.push(example); | ||
| } | ||
| // Always add to flat list for compatibility | ||
| result.examples.push(example); | ||
| } | ||
| else if (cleanLine.startsWith('[SYNONYM]')) { | ||
| result.synonyms.push(cleanLine.replace('[SYNONYM]', '').trim()); | ||
| } | ||
| else if (cleanLine.startsWith('[ANTONYM]')) { | ||
| result.antonyms.push(cleanLine.replace('[ANTONYM]', '').trim()); | ||
| } | ||
| else if (cleanLine.startsWith('[IDIOM]')) { | ||
| result.idioms.push(cleanLine.replace('[IDIOM]', '').trim()); | ||
| } | ||
| else if (cleanLine.startsWith('[TAG]')) { | ||
| result.tags.push(cleanLine.replace('[TAG]', '').trim()); | ||
| } | ||
| else if (cleanLine.startsWith('[ORIGIN]')) { | ||
| result.origin = cleanLine.replace('[ORIGIN]', '').trim(); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| } |
@@ -6,2 +6,6 @@ export interface Definition { | ||
| } | ||
| export interface TranslationItem { | ||
| type: 'antonym' | 'definition' | 'idiom' | 'origin' | 'synonym'; | ||
| value: Definition | string; | ||
| } | ||
| export interface TranslationResult { | ||
@@ -12,2 +16,3 @@ antonyms: string[]; | ||
| idioms: string[]; | ||
| items?: TranslationItem[]; | ||
| meaning: string[]; | ||
@@ -20,10 +25,13 @@ origin?: string; | ||
| } | ||
| export type LineStyle = 'bold' | 'code' | 'cyan-bold' | 'dim' | 'green' | 'magenta-bold' | 'normal' | 'yellow'; | ||
| export interface StyledLine { | ||
| style?: LineStyle; | ||
| text: string; | ||
| } | ||
| export interface BoxSection { | ||
| content: string | string[]; | ||
| style?: 'code' | 'dim' | 'normal'; | ||
| title?: string; | ||
| content: (string | StyledLine)[]; | ||
| title?: string | StyledLine; | ||
| } | ||
| export interface StandardBoxData { | ||
| content: BoxSection[]; | ||
| footer?: string[]; | ||
| subtitle?: string; | ||
@@ -30,0 +38,0 @@ tags?: string[]; |
| import { RenderStrategy } from '../interfaces/render-strategy.js'; | ||
| import { StandardBoxData, TranslationResult } from '../providers/types.js'; | ||
| export declare class BoxRenderStrategy implements RenderStrategy<StandardBoxData | TranslationResult> { | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export declare class BoxRenderStrategy implements RenderStrategy { | ||
| id: string; | ||
| private lastHeight; | ||
| render(result: StandardBoxData | TranslationResult, fromCache: boolean): void; | ||
| render(data: StandardBoxData, fromCache: boolean): void; | ||
| reset(): void; | ||
| private isStandardBoxData; | ||
| private normalize; | ||
| private normalizeTranslationResult; | ||
| } |
+54
-87
| import boxen from 'boxen'; | ||
| import chalk from 'chalk'; | ||
| import wrap from 'wrap-ansi'; | ||
| const PADDING = 1; | ||
| const BORDER_WIDTH = 1; | ||
| const TOTAL_HORIZONTAL_MARGIN = (PADDING + BORDER_WIDTH) * 2; | ||
| function applyStyle(line) { | ||
| if (typeof line === 'string') { | ||
| return line; | ||
| } | ||
| switch (line.style) { | ||
| case 'bold': { | ||
| return chalk.bold(line.text); | ||
| } | ||
| case 'code': { | ||
| return chalk.gray(line.text); | ||
| } | ||
| case 'cyan-bold': { | ||
| return chalk.cyan.bold(line.text); | ||
| } | ||
| case 'dim': { | ||
| return chalk.dim(line.text); | ||
| } | ||
| case 'green': { | ||
| return chalk.green(line.text); | ||
| } | ||
| case 'magenta-bold': { | ||
| return chalk.magenta.bold(line.text); | ||
| } | ||
| case 'yellow': { | ||
| return chalk.yellow(line.text); | ||
| } | ||
| default: { | ||
| return line.text; | ||
| } | ||
| } | ||
| } | ||
| export class BoxRenderStrategy { | ||
| id = 'box'; | ||
| lastHeight = 0; | ||
| render(result, fromCache) { | ||
| const data = this.normalize(result); | ||
| render(data, fromCache) { | ||
| const termWidth = process.stdout.columns || 80; | ||
| const contentWidth = termWidth - TOTAL_HORIZONTAL_MARGIN; | ||
| const wrapContent = (text) => wrap(text, contentWidth, { hard: true, trim: false }); | ||
| const headerParts = [chalk.bold(data.title || '...')]; | ||
@@ -16,30 +53,17 @@ if (data.subtitle) | ||
| headerParts.push(chalk.dim('(cached)')); | ||
| const header = headerParts.join(' '); | ||
| const content = [header, '']; | ||
| if (data.content && data.content.length > 0) { | ||
| for (const section of data.content) { | ||
| if (section.title) { | ||
| content.push(chalk.cyan.bold(section.title)); | ||
| } | ||
| const lines = Array.isArray(section.content) ? section.content : [section.content]; | ||
| for (const line of lines) { | ||
| if (section.style === 'dim') { | ||
| content.push(chalk.dim(line)); | ||
| } | ||
| else if (section.style === 'code') { | ||
| content.push(chalk.gray(line)); | ||
| } | ||
| else { | ||
| content.push(line); | ||
| } | ||
| } | ||
| content.push(''); | ||
| } | ||
| } | ||
| if (data.footer && data.footer.length > 0) { | ||
| if (content.at(-1) !== '') | ||
| content.push(''); | ||
| content.push(...data.footer); | ||
| } | ||
| const box = boxen(content.join('\n').trim(), { borderColor: fromCache ? 'gray' : 'cyan', borderStyle: 'round', padding: 1 }); | ||
| const header = wrapContent(headerParts.join(' ')); | ||
| const sectionContents = (data.content || []).map(section => { | ||
| const title = section.title ? applyStyle(section.title) : ''; | ||
| const content = section.content.map(item => applyStyle(item)).join('\n'); | ||
| const sectionText = [title, content].filter(Boolean).join('\n'); | ||
| return wrapContent(sectionText); | ||
| }); | ||
| const finalContent = [header, ...sectionContents] | ||
| .filter(Boolean) | ||
| .join('\n\n'); | ||
| const box = boxen(finalContent, { | ||
| borderColor: fromCache ? 'gray' : 'cyan', | ||
| borderStyle: 'round', | ||
| padding: PADDING, | ||
| }); | ||
| if (this.lastHeight > 0) { | ||
@@ -51,4 +75,2 @@ process.stdout.write(`\u001B[${this.lastHeight}A`); | ||
| this.lastHeight = box.split('\n').length; | ||
| if (fromCache) | ||
| this.lastHeight = 0; | ||
| } | ||
@@ -58,57 +80,2 @@ reset() { | ||
| } | ||
| isStandardBoxData(input) { | ||
| return 'content' in input && Array.isArray(input.content); | ||
| } | ||
| normalize(input) { | ||
| if (this.isStandardBoxData(input)) { | ||
| return input; | ||
| } | ||
| return this.normalizeTranslationResult(input); | ||
| } | ||
| normalizeTranslationResult(tr) { | ||
| const sections = []; | ||
| if (tr.definitions && tr.definitions.length > 0) { | ||
| for (const def of tr.definitions) { | ||
| const lines = [chalk.green(' • ') + def.meaning]; | ||
| if (def.examples && def.examples.length > 0) { | ||
| for (const ex of def.examples) { | ||
| lines.push(chalk.dim(' - ') + ex); | ||
| } | ||
| } | ||
| sections.push({ | ||
| content: lines, | ||
| title: def.partOfSpeech, | ||
| }); | ||
| } | ||
| } | ||
| else if (tr.meaning && tr.meaning.length > 0) { | ||
| const lines = tr.meaning.map(m => chalk.green('• ') + m); | ||
| if (tr.examples && tr.examples.length > 0) { | ||
| lines.push('', chalk.yellow('Examples:')); | ||
| for (const ex of tr.examples) | ||
| lines.push(chalk.dim('• ') + ex); | ||
| } | ||
| sections.push({ content: lines }); | ||
| } | ||
| if (tr.idioms && tr.idioms.length > 0) { | ||
| const lines = tr.idioms.map(i => chalk.dim('• ') + i); | ||
| sections.push({ | ||
| content: [chalk.magenta.bold('Idioms:'), ...lines], | ||
| }); | ||
| } | ||
| const footer = []; | ||
| if (tr.synonyms && tr.synonyms.length > 0) | ||
| footer.push(chalk.dim('Synonyms: ') + tr.synonyms.join(', ')); | ||
| if (tr.antonyms && tr.antonyms.length > 0) | ||
| footer.push(chalk.dim('Antonyms: ') + tr.antonyms.join(', ')); | ||
| if (tr.origin) | ||
| footer.push(chalk.dim('Origin: ') + tr.origin); | ||
| return { | ||
| content: sections, | ||
| footer, | ||
| subtitle: tr.phonetic, | ||
| tags: tr.tags, | ||
| title: tr.word, | ||
| }; | ||
| } | ||
| } |
| import { RenderStrategy } from '../interfaces/render-strategy.js'; | ||
| export declare const RendererFactory: { | ||
| get(id: string): RenderStrategy<any>; | ||
| get(id: string): RenderStrategy; | ||
| }; |
| import { BoxRenderStrategy } from './box.js'; | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars | ||
| const strategies = { | ||
| 'box': new BoxRenderStrategy(), | ||
| }; | ||
| export const RendererFactory = { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| get(id) { | ||
| // Create new instance to maintain independent state (like lastHeight) | ||
| // Or we could reset state. Creating new is safer. | ||
| if (id === 'box') | ||
@@ -12,0 +6,0 @@ return new BoxRenderStrategy(); |
+26
-22
@@ -25,17 +25,12 @@ import crypto from 'node:crypto'; | ||
| async play(text, lang = 'en') { | ||
| try { | ||
| const filePath = await this.getAudioFile(text, lang); | ||
| return new Promise((resolve, reject) => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| audioPlayer.play(filePath, (err) => { | ||
| if (err) | ||
| reject(err); | ||
| else | ||
| resolve(); | ||
| }); | ||
| const filePath = await this.getAudioFile(text, lang); | ||
| return new Promise((resolve, reject) => { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| audioPlayer.play(filePath, (err) => { | ||
| if (err) | ||
| reject(err); | ||
| else | ||
| resolve(); | ||
| }); | ||
| } | ||
| catch (error) { | ||
| console.error('Failed to play audio:', error); | ||
| } | ||
| }); | ||
| } | ||
@@ -53,12 +48,21 @@ async getAudioFile(text, lang) { | ||
| const url = `http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=${encodeURIComponent(text)}&tl=${lang}`; | ||
| // eslint-disable-next-line n/no-unsupported-features/node-builtins | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| throw new Error(`TTS API failed: ${response.statusText}`); | ||
| try { | ||
| // eslint-disable-next-line n/no-unsupported-features/node-builtins | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| throw new Error(`TTS API failed: ${response.statusText}`); | ||
| } | ||
| const arrayBuffer = await response.arrayBuffer(); | ||
| const buffer = Buffer.from(arrayBuffer); | ||
| fs.writeFileSync(filePath, buffer); | ||
| return filePath; | ||
| } | ||
| const arrayBuffer = await response.arrayBuffer(); | ||
| const buffer = Buffer.from(arrayBuffer); | ||
| fs.writeFileSync(filePath, buffer); | ||
| return filePath; | ||
| catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| if (errorMessage.includes('fetch failed')) { | ||
| throw new Error('Network error: Unable to access Google TTS service. Please check your network connection.'); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| } |
@@ -1,5 +0,5 @@ | ||
| import { TranslationResult } from '../providers/types.js'; | ||
| import { StandardBoxData } from '../providers/types.js'; | ||
| export interface HistoryEntry { | ||
| provider: string; | ||
| result: TranslationResult; | ||
| result: StandardBoxData; | ||
| source: string; | ||
@@ -20,3 +20,3 @@ targetLang: string; | ||
| getLatest(): HistoryEntry | undefined; | ||
| save(text: string, result: TranslationResult, provider: string, targetLang: string): void; | ||
| save(text: string, result: StandardBoxData, provider: string, targetLang: string): void; | ||
| } |
+28
-19
@@ -200,5 +200,7 @@ import { checkbox, confirm, input as inputPrompt, password, select } from '@inquirer/prompts'; | ||
| choices: [ | ||
| { name: 'Translation', value: 'translation' }, | ||
| { name: 'Grammar Check', value: 'grammar' }, | ||
| { name: 'Oxford Dictionary', value: 'oxford' } | ||
| { name: 'Translation (通用翻译)', value: 'translation' }, | ||
| { name: 'Youdao (有道词典风格)', value: 'youdao' }, | ||
| { name: 'Oxford (牛津词典风格)', value: 'oxford' }, | ||
| { name: 'Cambridge (剑桥词典风格)', value: 'cambridge' }, | ||
| { name: 'Grammar Check (语法检查)', value: 'grammar' } | ||
| ], | ||
@@ -550,4 +552,2 @@ message: 'Select Mode:', | ||
| const mode = configService.getMode(); | ||
| // Strategies (Hardcoded for now) | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const promptStrategy = PromptFactory.get(mode); | ||
@@ -566,8 +566,15 @@ const renderStrategy = RendererFactory.get('box'); | ||
| const stream = provider.stream(text, { systemMessage }); | ||
| let buffer = ''; | ||
| // Use streaming parser if available | ||
| const streamParser = promptStrategy.createStreamParser?.({ targetLang }); | ||
| let result = promptStrategy.getInitialState(); | ||
| let lastRender = 0; | ||
| for await (const chunk of stream) { | ||
| buffer += chunk; | ||
| result = promptStrategy.parse(buffer); | ||
| if (streamParser) { | ||
| streamParser.feed(chunk); | ||
| result = streamParser.getResult(); | ||
| } | ||
| else { | ||
| // Fallback: accumulate and parse | ||
| result = promptStrategy.parse(chunk, { targetLang }); | ||
| } | ||
| const now = Date.now(); | ||
@@ -579,9 +586,9 @@ if (now - lastRender > 50) { | ||
| } | ||
| // Force override word with input text to ensure clean header | ||
| if (result && typeof result === 'object' && 'word' in result) { | ||
| result.word = text; | ||
| // Final result - override title with input text for word lookups | ||
| if (result.title === '' || !result.title) { | ||
| result.title = text; | ||
| } | ||
| renderStrategy.render(result, false); | ||
| renderStrategy.reset(); // Clean up after done | ||
| // 2. Save to History | ||
| // Save to History | ||
| historyService.save(text, result, configService.config.current, targetLang); | ||
@@ -592,7 +599,2 @@ } | ||
| process.stdout.cursorTo(0); | ||
| // We pass a dummy spinner or undefined, handleProviderError needs to support that | ||
| // But since handleProviderError might use spinner, we should check its signature | ||
| // Actually handleProviderError in utils takes spinner? | ||
| // Let's assume it handles missing spinner or we pass null. | ||
| // In repl we don't have a spinner usually. | ||
| if (await handleProviderError(error, configService)) { | ||
@@ -614,7 +616,14 @@ await handleTranslation(text, configService); | ||
| } | ||
| catch { | ||
| catch (error) { | ||
| process.stdout.clearLine(0); | ||
| process.stdout.cursorTo(0); | ||
| console.log(chalk.red('Audio playback failed. System player might be missing (requires afplay/mpg123).')); | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| if (errorMessage.includes('Network error')) { | ||
| console.log(chalk.red(`✗ ${errorMessage}`)); | ||
| } | ||
| else { | ||
| console.log(chalk.red('✗ Audio playback failed. System player might be missing (requires afplay, mpg123).')); | ||
| console.log(chalk.dim(` Details: ${errorMessage}`)); | ||
| } | ||
| } | ||
| } |
@@ -31,3 +31,3 @@ { | ||
| }, | ||
| "version": "1.1.0" | ||
| "version": "1.2.0" | ||
| } |
+2
-1
| { | ||
| "name": "xtrans", | ||
| "description": "Minimalist AI-powered terminal translator / 极简 AI 终端翻译工具", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "author": "wengqianshan", | ||
@@ -23,2 +23,3 @@ "bin": { | ||
| "uuid": "^13.0.0", | ||
| "wrap-ansi": "^9.0.2", | ||
| "zod": "^4.3.6" | ||
@@ -25,0 +26,0 @@ }, |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
93302
19.32%53
17.78%2327
19.15%13
8.33%6
100%+ Added