fix(store): localised docs, debounced typing, explicit-submit always AI
Some checks failed
Security / Security check (push) Failing after 1s

Three Stefan-found bugs in one pass:

1. Documentation rendered in English even when Studio was set
   to German. _loadDocs now passes Localizations.localeOf(...)
   to fetchModuleDocs(); the hub tries README.de.md before
   README.md when locale='de'.

2. 'Keine Treffer' flickered on every keystroke. _onAskTyping
   used to call _runSearch synchronously per character — a
   short 2-3-letter substring matched nothing, so the empty-
   state showed for a frame, then the next character revealed
   results. Add a 250ms typing debounce (Timer + cancel on
   dispose) so the search only re-runs once typing pauses.

3. Submit button did substring search even when System-AI was
   enabled — the _looksLikeQuestion heuristic only routed
   4+-word / question-mark queries through the LLM. The user
   reasonably expected an explicit submit (with the AI-icon
   showing on the button) to actually use AI. New behaviour:
   when system-AI is enabled, the submit button always calls
   _runAiQuery; live-typing keeps substring filtering. The
   heuristic helper was removed (now unused).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-24 22:07:40 +02:00
parent ec7417fef5
commit 98a6414eaa
2 changed files with 35 additions and 17 deletions

View file

@ -350,8 +350,8 @@ class HubService {
/// back as `errorKind` strings (not exceptions) so callers can
/// render fallback UI without try/catch ceremony.
Future<({String errorKind, String text, String sourceUrl})>
fetchModuleDocs(String name) async {
final r = await _client.fetchModuleDocs(name);
fetchModuleDocs(String name, {String locale = ''}) async {
final r = await _client.fetchModuleDocs(name, locale: locale);
return (
errorKind: r.errorKind,
text: r.text,

View file

@ -5,6 +5,7 @@
// renders the full bilingual description, requires-list, and
// repository link.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@ -74,6 +75,11 @@ class _StorePageState extends State<StorePage> {
List<_AiMatch>? _aiMatches;
Set<String>? _aiMatchedNames;
String? _aiError;
/// Debounce-Timer für Such-Filterung beim Tippen. Ohne
/// Debounce flackert "Keine Treffer" zwischen Buchstaben
/// (für 2-3 Buchstaben matched z.B. nichts, das nächste
/// dann doch). 250ms ist comfortable für Tippen.
Timer? _typingDebounce;
/// Whether the operator has a System-AI configured. Drives
/// the Ask-bar hint copy and disables the AI path when off.
bool _systemAiEnabled = false;
@ -105,6 +111,7 @@ class _StorePageState extends State<StorePage> {
@override
void dispose() {
_typingDebounce?.cancel();
_queryCtrl.dispose();
super.dispose();
}
@ -442,7 +449,6 @@ class _StorePageState extends State<StorePage> {
/// operator submits otherwise typing "ein Modul, das …"
/// wipes the visible result set with every space.
void _onAskTyping(String value) {
final trimmed = value.trim();
if (_aiAnswer != null || _aiMatches != null) {
setState(() {
_aiAnswer = null;
@ -451,17 +457,35 @@ class _StorePageState extends State<StorePage> {
_aiError = null;
});
}
if (_systemAiEnabled && _looksLikeQuestion(trimmed)) return;
// Tippen läuft IMMER nur durch die lokale Substring-Suche.
// Die KI-Suche kostet einen LLM-Roundtrip und gehört hinter
// den expliziten Submit-Knopf sonst feuern wir pro
// Keystroke einen Prompt. Debounce verhindert "Keine
// Treffer" zwischen Buchstaben.
_typingDebounce?.cancel();
_typingDebounce = Timer(const Duration(milliseconds: 250), () {
if (!mounted) return;
_runSearch();
});
}
Future<void> _onAskSubmit() async {
// Pending typing-debounce abbrechen wir submitten jetzt
// explizit und wollen nicht hinterher nochmal substring-
// filter laufen lassen.
_typingDebounce?.cancel();
final q = _queryCtrl.text.trim();
if (q.isEmpty) {
_clearQuery();
return;
}
if (_systemAiEnabled && _looksLikeQuestion(q)) {
// Wenn System-AI verfügbar ist, geht der explizite Submit
// IMMER über die KI-Suche egal ob die Eingabe wie eine
// Frage aussieht. Das war der Bug: Nutzer tippt "tabellen"
// + klick Suchen, erwartet KI, bekommt Substring-Filter.
// Substring-Filter passiert weiter live beim Tippen
// (siehe _onAskTyping).
if (_systemAiEnabled) {
await _runAiQuery(q);
} else {
_runSearch();
@ -479,16 +503,6 @@ class _StorePageState extends State<StorePage> {
_runSearch();
}
/// Heuristic question detector kept generous so a sentence
/// "modul für tabellen lesen" triggers the LLM path even
/// without a question mark. Single-word and two-word inputs
/// stay on substring search where they belong.
bool _looksLikeQuestion(String q) {
if (q.endsWith('?')) return true;
final words = q.split(RegExp(r'\s+'));
return words.length >= 4;
}
Future<void> _runAiQuery(String query) async {
setState(() {
_aiThinking = true;
@ -1922,10 +1936,14 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
void _loadDocs() {
if (_docsLoaded) return;
// Sprache vom aktuellen Studio-Locale ans Hub weitergeben,
// damit `README.de.md` zuerst probiert wird wenn Studio auf
// Deutsch steht.
final locale = Localizations.localeOf(context).languageCode;
setState(() {
_docsLoaded = true;
_docsFuture =
HubService.instance.fetchModuleDocs(widget.item.name);
HubService.instance.fetchModuleDocs(widget.item.name, locale: locale);
});
}