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. 119
      src/lib/languagetool-cm-linter.ts
  4. 14
      src/lib/translate-client.ts

35
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

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

8
src/lib/languagetool-client.ts

@ -46,7 +46,13 @@ export async function languageToolCheck( @@ -46,7 +46,13 @@ export async function languageToolCheck(
logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) })
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 {

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

@ -1,71 +1,111 @@ @@ -1,71 +1,111 @@
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'
/** Local LanguageTool is slow on cold JVM; keep payloads bounded (LT has ~20–30k limits anyway). */
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 {
const from = Math.max(0, Math.min(m.offset, docLen))
const to = Math.max(from, Math.min(m.offset + m.length, docLen))
if (to <= from) return null
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 {
from,
to,
severity: 'info',
message: m.message + (m.rule?.id ? ` (${m.rule.id})` : ''),
actions: fix
? [
{
name: 'Apply',
apply(view) {
view.dispatch({ changes: { from, to, insert: fix } })
}
}
]
: undefined
message,
renderMessage(view: EditorView) {
const wrap = document.createElement('span')
wrap.style.display = 'inline-flex'
wrap.style.alignItems = 'baseline'
wrap.style.gap = '0.4em'
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
}
}
}
/**
* 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.
* `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 inFlight: AbortController | null = null
return linter((view) => {
return new Promise<Diagnostic[]>((resolve) => {
let settled = false
const finish = (diags: Diagnostic[]) => {
if (settled) return
settled = true
resolve(diags)
}
const text = view.state.doc.toString()
if (text.length < 3) {
finish([])
return
}
const seq = ++requestSeq
inFlight?.abort()
inFlight = null
return linter(
(view) => {
return new Promise<Diagnostic[]>((resolve) => {
let settled = false
const finish = (diags: Diagnostic[]) => {
if (settled) return
settled = true
resolve(diags)
}
window.setTimeout(() => {
if (seq !== requestSeq) {
const text = view.state.doc.toString()
if (text.length < 3) {
finish([])
return
}
const seq = ++requestSeq
inFlight?.abort()
inFlight = null
const toSend = text.length > MAX_CHECK_CHARS ? text.slice(0, MAX_CHECK_CHARS) : text
const ac = new AbortController()
inFlight = ac
void languageToolCheck(toSend, language, ac.signal)
void languageToolCheck(toSend, getLanguage(), ac.signal)
.then((res) => {
if (seq !== requestSeq) {
finish([])
@ -82,7 +122,12 @@ export function languageToolLintExtension(language: string, debounceMs: number): @@ -82,7 +122,12 @@ export function languageToolLintExtension(language: string, debounceMs: number):
.catch(() => {
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( @@ -119,6 +119,13 @@ export async function translatePlainText(
const key = cacheKey(text, resolvedSource, resolvedTarget)
const hit = memoryCache.get(key)
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
}
@ -145,5 +152,12 @@ export async function translatePlainText( @@ -145,5 +152,12 @@ export async function translatePlainText(
const out = data.translatedText ?? ''
pruneMemory()
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
}

Loading…
Cancel
Save