From e868781672e1e5898653cc679534ef4a8644ce7c Mon Sep 17 00:00:00 2001 From: Bernhard Jackiewicz Date: Sun, 4 Oct 2026 17:42:03 +0200 Subject: [PATCH] fix(ui): answer questions about the Galaxy view from its scope and catch restated answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With .github selected in the Galaxy hierarchy, "kkannst du mir die heirarchie erklären" and "erkläre die aktuelle hierarchy" went to the free model, which answered "Ich kann dir die Heirarchy erklären." and "Die aktuelle Hierarchie erklärt.". Questions about the current view (die Hierarchie, die Ansicht, den Graphen, den Ausschnitt, die Struktur, this view, the hierarchy, what am I looking at, was sehe ich hier), typos included, now get a listed answer from the loaded scope in the language of the question: the root with its kind in the middle, incoming and outgoing relationships by type with count and bounded names, how far the scope reaches and how the picture is read. The Galaxy evidence carries which picture is shown; switching it keeps the content identity, so the automatic explanation is not written again. A free model answer that only restates its question is no longer shown as an answer: the turn says the model gave no answer, lists the facts of the selection and offers Ask again, which tells the model not to restate the question. A restating sentence under the facts of a general question is left out. Short real answers such as "Ja, 11." stay. Signed-off-by: Bernhard Jackiewicz --- graph-ui/src/browser-ai/BrowserChatDock.tsx | 48 ++++++++-- .../browser-ai/BrowserChatDock.view.test.tsx | 96 +++++++++++++++++++ graph-ui/src/browser-ai/chat-model.ts | 17 +++- graph-ui/src/browser-ai/echo-answer.test.ts | 35 +++++++ graph-ui/src/browser-ai/echo-answer.ts | 51 ++++++++++ .../src/browser-ai/galaxy-evidence.fixture.ts | 16 ++++ graph-ui/src/browser-ai/galaxy-evidence.ts | 16 +++- graph-ui/src/browser-ai/question-intent.ts | 51 +++++++++- graph-ui/src/browser-ai/strings.ts | 40 ++++++++ graph-ui/src/browser-ai/view-answer.test.ts | 90 +++++++++++++++++ graph-ui/src/browser-ai/view-answer.ts | 55 +++++++++++ .../galaxy/GalaxyPanel.chat-display.test.tsx | 58 +++++++++++ graph-ui/src/galaxy/GalaxyPanel.tsx | 6 +- graph-ui/src/galaxy/selection-evidence.ts | 12 ++- 14 files changed, 570 insertions(+), 21 deletions(-) create mode 100644 graph-ui/src/browser-ai/BrowserChatDock.view.test.tsx create mode 100644 graph-ui/src/browser-ai/echo-answer.test.ts create mode 100644 graph-ui/src/browser-ai/echo-answer.ts create mode 100644 graph-ui/src/browser-ai/view-answer.test.ts create mode 100644 graph-ui/src/browser-ai/view-answer.ts create mode 100644 graph-ui/src/galaxy/GalaxyPanel.chat-display.test.tsx diff --git a/graph-ui/src/browser-ai/BrowserChatDock.tsx b/graph-ui/src/browser-ai/BrowserChatDock.tsx index 69ee0a5b2..d2c6229bb 100644 --- a/graph-ui/src/browser-ai/BrowserChatDock.tsx +++ b/graph-ui/src/browser-ai/BrowserChatDock.tsx @@ -3,7 +3,7 @@ import type { BrowserAiProgress } from './browser-ai-controller'; import { createBrowserChatRuntime, type BrowserChatRuntime } from './browser-ai-runtime'; import { BROWSER_MODELS, getBrowserModel, isBrowserModelCached, removeBrowserModelCache, type BrowserModel } from './model-policy'; import { keepAgent, loadProgress, offerAgent, peekAgent, takeAgent, type AgentHandover, type LoadProgress } from './agent-handover'; -import { buildChatMessages, selectionLocation, sentInHistory, snapshotAttachment, snapshotReaderContext, trimChatHistory, type BrowserChatAttachment, type BrowserChatContext, type BrowserChatReaderContext, type BrowserChatSource, type BrowserChatTurn } from './chat-model'; +import { buildChatMessages, echoRetryRequest, selectionLocation, sentInHistory, snapshotAttachment, snapshotReaderContext, trimChatHistory, type BrowserChatAttachment, type BrowserChatContext, type BrowserChatReaderContext, type BrowserChatSource, type BrowserChatTurn } from './chat-model'; import { explanationInput, EXPLANATION_DELAY_MS, type ExplanationInput } from './proactive-selection'; import { prepareExplanationContext, selectionSummary, type PreparedExplanationContext } from './explanation-context'; import { AUTO_INPUT_TOKENS, AUTO_OUTPUT_TOKENS, citedInterpretation, explanationMode, explanationSentence, namesNotIn, parseExplanationResponse, explanationMessages, formatExplanationEvidence, type DroppedReason } from './explanation-response'; @@ -11,7 +11,7 @@ import { carriedSource, selectionSubject, SYMBOL_SOURCE_LINES, sourceTargetOf, s import { codeFacts, codeFactsMarkdown, codeSourceFacts } from './code-facts'; import { relationshipAnswer, relationshipSuggestion } from './relationship-answer'; import { clampTokenLimits, tokenLimitBounds, tokenLimitsFor, useAgentPreferences, type TokenLimits } from './agent-preferences'; -import { browserChatText, chatRound3Text, evidenceNote, groundedText, relationshipWords, topicText } from './strings'; +import { browserChatText, chatRound3Text, evidenceNote, groundedText, relationshipWords, topicText, viewText } from './strings'; import { isGpuRuntimeFailure, BrowserRuntimeFatalError } from './runtime-fault'; import ChatMarkdown from './ChatMarkdown'; import AgentSettingsDialog from './AgentSettingsDialog'; @@ -20,7 +20,9 @@ import { chatTopic, contextFreeFollowUp, followedTopic, missingContextAnswer, qu import { isDataFile, readerFacts } from './file-facts'; import { fileOutline } from './file-outline'; import { readGalaxyEvidence, type GalaxyEvidence } from './galaxy-evidence'; -import { followUpAnswer, generalQuestion, knownNames, noQuestion, noQuestionAnswer, type ExampleSubject } from './question-intent'; +import { followUpAnswer, generalQuestion, knownNames, noQuestion, noQuestionAnswer, viewQuestion, type ExampleSubject } from './question-intent'; +import { viewAnswer } from './view-answer'; +import { echoAnswer } from './echo-answer'; import './browser-chat.css'; export type { BrowserChatAttachment, BrowserChatContext, BrowserChatReaderContext, BrowserChatSource } from './chat-model'; @@ -90,6 +92,17 @@ function groundedExplanation(summary: readonly string[], sentence: string | unde const note = sentence ? words.factsAndSentence : dropped ? words.sentenceDropped(reason) : words.factsOnly; return [summary.map(line => `- ${line}`).join('\n'), ...code ? [code] : [], ...sentence ? [sentence] : [], `_${note}_`].join('\n\n'); } +/** What a turn says when the model only restated the question: that it gave no answer, then the + * facts of the selection or open file the question was about, as a general question lists them (H2). */ +function echoedAnswer(output: string, language: 'en' | 'de', graph: BrowserChatContext | undefined, reader: BrowserChatReaderContext | undefined): string { + const text = viewText[language]; + const echoed = output.replace(/\s+/g, ' ').trim(); + const shown = echoed.length > 80 ? `${echoed.slice(0, 79)}…` : echoed; + const source = reader?.source; + const summary = graph ? selectionSummary(graph, language) : source ? isDataFile(source.path) ? readerFacts(reader) : codeSourceFacts(source, language).summary : []; + if (!summary.length) return `${text.noAnswer(shown)} ${text.rephrase}`; + return `${text.noAnswer(shown)} ${text.factsFollow}\n\n${groundedExplanation(summary, undefined, false, graph ? 'graph' : 'file', language)}`; +} /** The same while the model writes its sentence. */ const writingExplanation = (summary: readonly string[], note: string, code?: string): string => summary.length ? [summary.map(line => `- ${line}`).join('\n'), ...code ? [code] : [], `_${note}_`].join('\n\n') : ''; @@ -663,6 +676,17 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = " const language = questionLanguage(prompt); const galaxy = currentGraph[0] ? readGalaxyEvidence(currentGraph[0].text) : undefined; const fileSource = reader?.source; + // A question about the current view ("erkläre die aktuelle Hierarchie", "what am I looking at") + // is answered from the loaded scope: the model only restated it (H1). + const view = !retry && !source && !extra.length && galaxy && viewQuestion(prompt, galaxyNames(galaxy)) === 'general' ? viewAnswer(galaxy, language) : undefined; + if (view) { + const id = `local-turn-${crypto.randomUUID()}`, scope = currentGraph[0]; + setTurns(previous => [...previous, { id, prompt, context: extra, topic, listedFrom: scope, replyLanguage: language, + evidence: prepareExplanationContext(undefined, scope, chatEvidence, knownSource(scope)), modelId: model.id, request: [], + answer: view, status: 'complete', answeredFrom: 'graph' }]); + consume(); listSource(id, scope); + return; + } // A prompt that asks nothing ("test", "hallo") gets questions it could ask, not an echo of the model (C6). const known = knownNames({ galaxy, names: galaxy ? galaxyNames(galaxy) : fileSource ? [fileSource.path] : [], texts: [fileSource?.text ?? '', source?.text ?? '', ...currentGraph.filter(() => !galaxy).map(item => item.label), ...extra.map(item => item.label)] }); @@ -726,7 +750,8 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = " consume(); return; } - let request = grounded ? explanationMessages(packet!, grounded.subject, language) : retry && !ask ? retry.request.map(message => ({ ...message })) : makeRequest(); + // An echo asked again is told not to restate the question: the same request gets the same echo (H2). + let request = grounded ? explanationMessages(packet!, grounded.subject, language) : retry && !ask ? retry.echo ? echoRetryRequest(retry.request) : retry.request.map(message => ({ ...message })) : makeRequest(); if (automaticRun) { automaticRun.cancelled = true; lastAttempt.current = undefined; setQuestionQueued(true); currentRuntime.stop(); @@ -791,10 +816,19 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = " // One sentence beside the facts, checked like the sentence of the explanation card. const result = parseExplanationResponse(output, packet!); const checked = !stopRequested.current && result.status === 'generated' ? explanationSentence(result.markdown, packet!, request.map(message => message.content).join('\n')) : {}; - const answer = groundedExplanation(grounded.summary, checked.sentence, checked.reason ?? checked.dropped !== undefined, grounded.from, language, code); + // A sentence that only restates the question says nothing beside the facts (H2). + const echoed = checked.sentence !== undefined && echoAnswer(checked.sentence, prompt); + const answer = groundedExplanation(grounded.summary, echoed ? undefined : checked.sentence, checked.reason ?? (echoed || checked.dropped !== undefined), grounded.from, language, code); setTurns(previous => previous.map(item => item.id === id ? { ...item, answer, status: stopRequested.current ? 'stopped' : 'complete' } : item)); return; } + // An answer that only restates the question is none: the turn says so, with the facts it was about (H2). + if (!stopRequested.current && echoAnswer(output, prompt)) { + const about = packetGraph[0] ?? retry?.listedFrom; + setTurns(previous => previous.map(item => item.id === id ? { ...item, answer: echoedAnswer(output, language, about, about ? undefined : reader), status: 'complete', echo: true, + replyLanguage: language, ...about ? { listedFrom: about } : {} } : item)); + return; + } setTurns(previous => previous.map(item => item.id === id ? { ...item, answer: stopRequested.current ? item.answer : output, status: stopRequested.current ? 'stopped' : 'complete', ...shortened && !stopRequested.current ? { shortened, limit: { inputTokens: limits.inputTokens, outputTokens: limits.outputTokens } } : {} } : item)); } catch (failure) { @@ -933,8 +967,8 @@ export default function BrowserChatDock({ proactiveSelection, selectionScope = " : null}
{turn.status === 'stopped' && turn.answer && Stopped · partial answer} - {!turn.answeredFrom && turn.answer && (turn.status === 'complete' || turn.status === 'stopped') && {ownWords.modelNote}} - {turn.status !== 'generating' && !turn.answeredFrom && {ownWords.modelNote}} + {turn.status !== 'generating' && !turn.answeredFrom && !turn.echo && message.content).join('\n')) : []} model={BROWSER_MODELS.find(candidate => candidate.id === turn.modelId)?.displayName ?? turn.modelId} historyOmitted={turn.historyOmitted} />} {turn.status === 'error' &&

{turn.error}

} {index === turns.length - 1 && turn.answeredFrom === 'suggestion' && turn.suggestion &&