Browse Source

more bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
7be4aac74e
  1. 35
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  2. 8
      src/lib/languagetool-client.ts
  3. 89
      src/lib/languagetool-cm-linter.ts
  4. 14
      src/lib/translate-client.ts

35
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -14,8 +14,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select' } from '@/components/ui/select'
import logger from '@/lib/logger'
import { isLanguageToolConfigured } from '@/lib/languagetool-client' import { isLanguageToolConfigured } from '@/lib/languagetool-client'
import { languageToolLintExtension } from '@/lib/languagetool-cm-linter' import { languageToolLintExtension, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter'
import { buildLanguageToolPreferenceList } from '@/lib/languagetool-language-order' import { buildLanguageToolPreferenceList } from '@/lib/languagetool-language-order'
import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice'
import { import {
@ -224,6 +225,8 @@ export default function AdvancedEventLabDialog({
[i18nLanguage, i18n.language] [i18nLanguage, i18n.language]
) )
const [ltLang, setLtLang] = useState(() => ltList[0] ?? 'en-US') const [ltLang, setLtLang] = useState(() => ltList[0] ?? 'en-US')
const ltLangRef = useRef(ltLang)
ltLangRef.current = ltLang
const [translateLangs, setTranslateLangs] = useState<TranslateLanguageOption[]>([]) const [translateLangs, setTranslateLangs] = useState<TranslateLanguageOption[]>([])
const [translateLoad, setTranslateLoad] = useState<'idle' | 'loading' | 'ready' | 'empty' | 'error'>('idle') const [translateLoad, setTranslateLoad] = useState<'idle' | 'loading' | 'ready' | 'empty' | 'error'>('idle')
const [translateSource, setTranslateSource] = useState('auto') const [translateSource, setTranslateSource] = useState('auto')
@ -235,6 +238,12 @@ export default function AdvancedEventLabDialog({
} }
}, [open, ltList]) }, [open, ltList])
useEffect(() => {
const v = markupView.current
if (!v || !open || !isLanguageToolConfigured()) return
requestAdvancedLabGrammarLint(v)
}, [ltLang, open])
useEffect(() => { useEffect(() => {
if (!open || !isTranslateConfigured()) { if (!open || !isTranslateConfigured()) {
setTranslateLangs([]) setTranslateLangs([])
@ -336,7 +345,7 @@ export default function AdvancedEventLabDialog({
}) })
] ]
if (isLanguageToolConfigured()) { if (isLanguageToolConfigured()) {
mkExtensions.push(languageToolLintExtension(ltLang, 650)) mkExtensions.push(languageToolLintExtension(() => ltLangRef.current, 650))
} }
if (dark) mkExtensions.push(oneDark) if (dark) mkExtensions.push(oneDark)
@ -407,7 +416,6 @@ export default function AdvancedEventLabDialog({
open, open,
initial, initial,
markupMode, markupMode,
ltLang,
dark, dark,
destroyEditors, destroyEditors,
t, t,
@ -449,6 +457,11 @@ export default function AdvancedEventLabDialog({
toast.message(t('Advanced lab translation same source target')) toast.message(t('Advanced lab translation same source target'))
return return
} }
logger.info('[AdvancedLab] translate button', {
source: translateSource,
target: translateTarget,
inputChars: text.length
})
try { try {
const out = await translatePlainText(text, translateTarget, translateSource) const out = await translatePlainText(text, translateTarget, translateSource)
if (!markupView.current) return if (!markupView.current) return
@ -487,7 +500,13 @@ export default function AdvancedEventLabDialog({
{isLanguageToolConfigured() ? ( {isLanguageToolConfigured() ? (
<div className="space-y-1 min-w-[10rem]"> <div className="space-y-1 min-w-[10rem]">
<Label htmlFor="lt-lang">{t('Advanced lab grammar language')}</Label> <Label htmlFor="lt-lang">{t('Advanced lab grammar language')}</Label>
<Select value={ltLang} onValueChange={setLtLang}> <Select
value={ltLang}
onValueChange={(code) => {
logger.info('[AdvancedLab] grammar language changed', { from: ltLang, to: code })
setLtLang(code)
}}
>
<SelectTrigger id="lt-lang" className="w-[220px]"> <SelectTrigger id="lt-lang" className="w-[220px]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@ -513,6 +532,10 @@ export default function AdvancedEventLabDialog({
<Select <Select
value={translateSource} value={translateSource}
onValueChange={(v) => { onValueChange={(v) => {
logger.info('[AdvancedLab] translation source language changed', {
from: translateSource,
to: v
})
setTranslateSource(v) setTranslateSource(v)
if (v !== 'auto' && v === translateTarget) { if (v !== 'auto' && v === translateTarget) {
const alt = translateLangs.find((l) => l.code !== v)?.code const alt = translateLangs.find((l) => l.code !== v)?.code
@ -538,6 +561,10 @@ export default function AdvancedEventLabDialog({
<Select <Select
value={translateTarget} value={translateTarget}
onValueChange={(v) => { onValueChange={(v) => {
logger.info('[AdvancedLab] translation target language changed', {
from: translateTarget,
to: v
})
setTranslateTarget(v) setTranslateTarget(v)
if (translateSource !== 'auto' && v === translateSource) { if (translateSource !== 'auto' && v === translateSource) {
setTranslateSource('auto') setTranslateSource('auto')

8
src/lib/languagetool-client.ts

@ -46,7 +46,13 @@ export async function languageToolCheck(
logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) }) logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) })
throw new Error(`LanguageTool: ${res.status}`) throw new Error(`LanguageTool: ${res.status}`)
} }
return (await res.json()) as LanguageToolCheckResponse const json = (await res.json()) as LanguageToolCheckResponse
logger.info('[AdvancedLab] LanguageTool check', {
language,
textChars: text.length,
matchCount: json.matches?.length ?? 0
})
return json
} }
export function isLanguageToolConfigured(): boolean { export function isLanguageToolConfigured(): boolean {

89
src/lib/languagetool-cm-linter.ts

@ -1,42 +1,88 @@
import { linter, type Diagnostic } from '@codemirror/lint' import { linter, type Diagnostic } from '@codemirror/lint'
import type { Extension } from '@codemirror/state' import { Annotation, EditorSelection, type Extension } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import { languageToolCheck, type LanguageToolMatch } from '@/lib/languagetool-client' import { languageToolCheck, type LanguageToolMatch } from '@/lib/languagetool-client'
/** Local LanguageTool is slow on cold JVM; keep payloads bounded (LT has ~20–30k limits anyway). */ /** Local LanguageTool is slow on cold JVM; keep payloads bounded (LT has ~20–30k limits anyway). */
const MAX_CHECK_CHARS = 28_000 const MAX_CHECK_CHARS = 28_000
/**
* Attach to a transaction so {@link languageToolLintExtension}'s `needsRefresh` schedules a new
* lint pass (e.g. grammar language changed). `forceLinting()` is a no-op once the last run finished.
*/
export const advancedLabGrammarLangRerun = Annotation.define<boolean>()
/** Request an immediate grammar re-check (after language change, etc.). */
export function requestAdvancedLabGrammarLint(view: EditorView): void {
view.dispatch({ annotations: advancedLabGrammarLangRerun.of(true) })
}
function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | null { function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | null {
const from = Math.max(0, Math.min(m.offset, docLen)) const from = Math.max(0, Math.min(m.offset, docLen))
const to = Math.max(from, Math.min(m.offset + m.length, docLen)) const to = Math.max(from, Math.min(m.offset + m.length, docLen))
if (to <= from) return null if (to <= from) return null
const fix = m.replacements?.[0]?.value const fix = m.replacements?.[0]?.value
const message = m.message + (m.rule?.id ? ` (${m.rule.id})` : '')
if (!fix) {
return { from, to, severity: 'info', message }
}
/**
* Do not use {@link Diagnostic.actions} for Apply: CodeMirror resolves actions via
* `findDiagnostic(..., diagnostic)` by **object identity**. Async LT refreshes replace
* diagnostics with new objects, so the tooltip's stale reference never matches and Apply
* never runs. A custom control in `renderMessage` uses stable closure `from`/`to`/`fix`.
*/
return { return {
from, from,
to, to,
severity: 'info', severity: 'info',
message: m.message + (m.rule?.id ? ` (${m.rule.id})` : ''), message,
actions: fix renderMessage(view: EditorView) {
? [ const wrap = document.createElement('span')
{ wrap.style.display = 'inline-flex'
name: 'Apply', wrap.style.alignItems = 'baseline'
apply(view) { wrap.style.gap = '0.4em'
view.dispatch({ changes: { from, to, insert: fix } })
} const btn = document.createElement('button')
btn.type = 'button'
btn.className = 'cm-diagnosticAction'
btn.textContent = 'Apply'
btn.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
const len = view.state.doc.length
const f = Math.max(0, Math.min(from, len))
const t = Math.max(f, Math.min(to, len))
view.dispatch({
changes: { from: f, to: t, insert: fix },
selection: EditorSelection.cursor(f + fix.length)
})
view.focus()
})
wrap.appendChild(btn)
const text = document.createElement('span')
text.textContent = message
wrap.appendChild(text)
return wrap
} }
]
: undefined
} }
} }
/** /**
* Async grammar/style lint for CodeMirror using LanguageTool `/v2/check`. * Async grammar/style lint for CodeMirror using LanguageTool `/v2/check`.
* Per-editor state (debounce / abort) lives in the closure so promises always settle and stale fetches are cancelled. * Per-editor state (debounce / abort) lives in the closure so promises always settle and stale fetches are cancelled.
* `getLanguage` is read on each lint pass so the UI can change language without remounting the editor.
*/ */
export function languageToolLintExtension(language: string, debounceMs: number): Extension { export function languageToolLintExtension(getLanguage: () => string, debounceMs: number): Extension {
let requestSeq = 0 let requestSeq = 0
let inFlight: AbortController | null = null let inFlight: AbortController | null = null
return linter((view) => { return linter(
(view) => {
return new Promise<Diagnostic[]>((resolve) => { return new Promise<Diagnostic[]>((resolve) => {
let settled = false let settled = false
const finish = (diags: Diagnostic[]) => { const finish = (diags: Diagnostic[]) => {
@ -55,17 +101,11 @@ export function languageToolLintExtension(language: string, debounceMs: number):
inFlight?.abort() inFlight?.abort()
inFlight = null inFlight = null
window.setTimeout(() => {
if (seq !== requestSeq) {
finish([])
return
}
const toSend = text.length > MAX_CHECK_CHARS ? text.slice(0, MAX_CHECK_CHARS) : text const toSend = text.length > MAX_CHECK_CHARS ? text.slice(0, MAX_CHECK_CHARS) : text
const ac = new AbortController() const ac = new AbortController()
inFlight = ac inFlight = ac
void languageToolCheck(toSend, language, ac.signal) void languageToolCheck(toSend, getLanguage(), ac.signal)
.then((res) => { .then((res) => {
if (seq !== requestSeq) { if (seq !== requestSeq) {
finish([]) finish([])
@ -82,7 +122,12 @@ export function languageToolLintExtension(language: string, debounceMs: number):
.catch(() => { .catch(() => {
finish([]) finish([])
}) })
}, debounceMs)
})
}) })
},
{
delay: debounceMs,
needsRefresh: (update) =>
update.transactions.some((tr) => tr.annotation(advancedLabGrammarLangRerun) === true)
}
)
} }

14
src/lib/translate-client.ts

@ -119,6 +119,13 @@ export async function translatePlainText(
const key = cacheKey(text, resolvedSource, resolvedTarget) const key = cacheKey(text, resolvedSource, resolvedTarget)
const hit = memoryCache.get(key) const hit = memoryCache.get(key)
if (hit && Date.now() - hit.at < CACHE_TTL_MS) { if (hit && Date.now() - hit.at < CACHE_TTL_MS) {
logger.info('[AdvancedLab] translate', {
source: resolvedSource,
target: resolvedTarget,
inputChars: text.length,
outputChars: hit.text.length,
cacheHit: true
})
return hit.text return hit.text
} }
@ -145,5 +152,12 @@ export async function translatePlainText(
const out = data.translatedText ?? '' const out = data.translatedText ?? ''
pruneMemory() pruneMemory()
memoryCache.set(key, { text: out, at: Date.now() }) memoryCache.set(key, { text: out, at: Date.now() })
logger.info('[AdvancedLab] translate', {
source: resolvedSource,
target: resolvedTarget,
inputChars: text.length,
outputChars: out.length,
cacheHit: false
})
return out return out
} }

Loading…
Cancel
Save