diff --git a/graph-ui/src/browser-ai/BrowserChatDock.view.test.tsx b/graph-ui/src/browser-ai/BrowserChatDock.view.test.tsx index 33a8403e2..37a796d46 100644 --- a/graph-ui/src/browser-ai/BrowserChatDock.view.test.tsx +++ b/graph-ui/src/browser-ai/BrowserChatDock.view.test.tsx @@ -16,7 +16,7 @@ describe('questions about the current view are answered from the loaded scope (H await dock.ask(question); const answer = dock.answerOf(dock.last()).replace(/\s+/g, ' '); expect(answer).toContain('.github (Ordner) steht in der Mitte; der Ausschnitt geht 1 Schritt in beide Richtungen.'); - expect(answer).toContain('Links, eingehend: CONTAINS_FOLDER (1): DETACHED'); + expect(answer).toContain('Links, eingehend: CONTAINS_FOLDER (1): django-demo · losgelöster HEAD'); expect(answer).toContain('Rechts, ausgehend: CONTAINS_FILE (4): CODE_OF_CONDUCT.md, FUNDING.yml, pull_request_template.md, SECURITY.md CONTAINS_FOLDER (1): workflows'); expect(answer).toContain('So liest du die Hierarchie: eingehende Beziehungen stehen links, ausgehende rechts.'); expect(answer).toContain('Aus dem indizierten Graphen gelistet, nicht vom Modell erzeugt.'); @@ -57,7 +57,7 @@ describe('a model answer that only restates the question is not shown as an answ expect(runtime.chat).toHaveBeenCalledOnce(); const answer = dock.answerOf(dock.last()); expect(answer).toContain('Das Modell hat keine Antwort gegeben, es hat nur die Frage wiederholt („Ich kann dir die Ordner erklären.“).'); - expect(answer).toContain('Ausgewählt: .github (Ordner) in .github.'); + expect(answer).toContain('Ausgewählt: .github (Ordner).'); expect(answer).toContain('Ausgehend: 5 Beziehungen zu 5 Symbolen (CONTAINS_FILE 4, CONTAINS_FOLDER 1).'); expect(answer).toContain('Aus dem indizierten Graphen gelistet, nicht vom Modell erzeugt.'); expect(dock.notesOf(dock.last())).toEqual([]); diff --git a/graph-ui/src/browser-ai/galaxy-evidence.folder.test.ts b/graph-ui/src/browser-ai/galaxy-evidence.folder.test.ts new file mode 100644 index 000000000..5a3317f98 --- /dev/null +++ b/graph-ui/src/browser-ai/galaxy-evidence.folder.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { readGalaxyEvidence, selectionSentence } from './galaxy-evidence'; +import { githubFolderEvidence } from './galaxy-evidence.fixture'; +import { relationshipWords } from './strings'; + +/* + * Handtest 04.10. (17:09): Die Karte zu .github sagte "Selected: .github (Folder) + * in .github." Bei einem Ordner oder einer Datei ist der Pfad das, was gewaehlt + * ist, kein Ort, an dem es steht; "in .github" wiederholte nur den Namen. + */ +describe('the selection sentence of a folder or a file', () => { + it('names a folder without repeating its own path as its place', () => { + const evidence = readGalaxyEvidence(githubFolderEvidence('hierarchy').text)!; + expect(selectionSentence(evidence, relationshipWords.en)[0]).toBe('Selected: .github (Folder).'); + expect(selectionSentence(evidence, relationshipWords.de)[0]).toBe('Ausgewählt: .github (Ordner).'); + }); +}); diff --git a/graph-ui/src/browser-ai/galaxy-evidence.ts b/graph-ui/src/browser-ai/galaxy-evidence.ts index ddb5b8d5d..1ee171a48 100644 --- a/graph-ui/src/browser-ai/galaxy-evidence.ts +++ b/graph-ui/src/browser-ai/galaxy-evidence.ts @@ -183,12 +183,16 @@ export function selectionName(evidence: GalaxyEvidence, words: RelationshipWords /** What is selected, in the words of the prompt (English) or of a question (C5). */ export function selectionSentence(evidence: GalaxyEvidence, words: RelationshipWords): string[] { const [first] = evidence.roots; - const range = (root: GalaxyEvidence['roots'][number]) => root.filePath + // A folder or a file is its path; "Selected: .github (Folder) in .github." only repeated the name (K47). + const isPath = (root: GalaxyEvidence['roots'][number]) => /^(?:folder|file|directory)$/i.test(root.kind ?? '') + && Boolean(root.filePath) && (root.filePath === root.name || root.filePath!.endsWith(`/${root.name}`)); + const shown = (root: GalaxyEvidence['roots'][number]) => isPath(root) ? root.filePath! : rootName(root, words); + const range = (root: GalaxyEvidence['roots'][number]) => root.filePath && !isPath(root) ? ` in ${root.filePath}${root.startLine ? `:${root.startLine}${root.endLine && root.endLine !== root.startLine ? `-${root.endLine}` : ''}` : ''}` : ''; // Kinds in the words of the answer: "(Klasse)" in a German one (W8). const kind = (root: GalaxyEvidence['roots'][number]) => root.kind ? ` (${words.kindName(root.kind)})` : ''; if (evidence.rootCount <= 1 && first) { - return [words.selected(`${rootName(first, words)}${kind(first)}${range(first)}`), + return [words.selected(`${shown(first)}${kind(first)}${range(first)}`), ...first.documentation ? [words.documentation(first.documentation)] : []]; } if (!first) return [words.notInScope(selectionName(evidence, words), words.kindName(evidence.selectionKind))]; diff --git a/graph-ui/src/browser-ai/view-answer.test.ts b/graph-ui/src/browser-ai/view-answer.test.ts index 25b9ba5bc..c47cdba14 100644 --- a/graph-ui/src/browser-ai/view-answer.test.ts +++ b/graph-ui/src/browser-ai/view-answer.test.ts @@ -43,7 +43,8 @@ describe('the listed answer about the current view (H1)', () => { const answer = viewAnswer(evidence('hierarchy'), 'de'); expect(answer.split('\n\n')).toEqual([ '`.github` (Ordner) steht in der Mitte; der Ausschnitt geht 1 Schritt in beide Richtungen.', - 'Links, eingehend:\n- **CONTAINS_FOLDER (1):** `DETACHED`', + // The branch node is named as the Galaxy names it, not by its bare "DETACHED" (K47). + 'Links, eingehend:\n- **CONTAINS_FOLDER (1):** `django-demo · losgelöster HEAD`', 'Rechts, ausgehend:\n- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`, `pull_request_template.md`, `SECURITY.md`\n- **CONTAINS_FOLDER (1):** `workflows`', 'So liest du die Hierarchie: eingehende Beziehungen stehen links, ausgehende rechts.', '7 Symbole und 6 Beziehungen, alle Beziehungstypen; vollständig geladen.', @@ -55,7 +56,7 @@ describe('the listed answer about the current view (H1)', () => { const answer = viewAnswer(evidence('galaxy'), 'en'); expect(answer.split('\n\n')).toEqual([ '`.github` (Folder) is in the middle; the scope reaches 1 hop in both directions.', - 'Incoming:\n- **CONTAINS_FOLDER (1):** `DETACHED`', + 'Incoming:\n- **CONTAINS_FOLDER (1):** `django-demo · detached HEAD`', 'Outgoing:\n- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`, `pull_request_template.md`, `SECURITY.md`\n- **CONTAINS_FOLDER (1):** `workflows`', 'The galaxy view shows this scope as a cloud. "hierarchy" at the top right lays the same scope out in columns: incoming on the left, outgoing on the right.', '7 symbols and 6 relationships, all relationship types; fully loaded.', @@ -84,7 +85,7 @@ describe('the listed answer about the current view (H1)', () => { it('says nothing about left, right or a cloud when the evidence does not say which view is shown', () => { const answer = viewAnswer(evidence(), 'en'); - expect(answer).toContain('Incoming:\n- **CONTAINS_FOLDER (1):** `DETACHED`'); + expect(answer).toContain('Incoming:\n- **CONTAINS_FOLDER (1):** `django-demo · detached HEAD`'); expect(answer).not.toMatch(/left|right|cloud/); }); }); diff --git a/graph-ui/src/browser-ai/view-answer.ts b/graph-ui/src/browser-ai/view-answer.ts index d8364497a..73ee5a830 100644 --- a/graph-ui/src/browser-ai/view-answer.ts +++ b/graph-ui/src/browser-ai/view-answer.ts @@ -1,5 +1,6 @@ +import { nodeDisplayName } from '../galaxy/node-names'; import type { RelationshipGroup } from '../galaxy/selection-evidence'; -import { scopeParts, sideLoaded, type GalaxyEvidence } from './galaxy-evidence'; +import { scopeParts, selectionName, sideLoaded, type GalaxyEvidence } from './galaxy-evidence'; import { relationshipWords, viewText, type RelationshipWords } from './strings'; /** The answer to a question about the current Galaxy view, listed from the loaded scope (H1). @@ -16,7 +17,8 @@ const quote = (value: string) => `\`${value.replace(/`/g, "'")}\``; /** "- **CONTAINS_FILE (4):** `CODE_OF_CONDUCT.md`, `FUNDING.yml`; +2 more". */ function typeLine(group: RelationshipGroup, words: RelationshipWords): string { - const shown = group.files.flatMap(file => file.symbols.map(symbol => symbol.name)).slice(0, NAMES_PER_TYPE); + // Named as the Galaxy names them: the branch node as "django-demo · detached HEAD", not "DETACHED" (K47). + const shown = group.files.flatMap(file => file.symbols.map(symbol => nodeDisplayName(symbol, words.nodeNames))).slice(0, NAMES_PER_TYPE); const rest = group.count - shown.length; const names = shown.length ? `${shown.map(quote).join(', ')}${rest > 0 ? `; ${words.more(rest)}` : ''}` : words.more(group.count); return `- **${words.typeCount(group.type, group.count)}:** ${names}`; @@ -28,7 +30,7 @@ export function viewAnswer(evidence: GalaxyEvidence, language: 'en' | 'de'): str const hierarchy = evidence.display === 'hierarchy'; const [first] = evidence.roots; const single = evidence.rootCount <= 1 && first !== undefined; - const center = text.center(quote(single ? first.name : evidence.label), words.kindName((single ? first.kind : undefined) ?? evidence.selectionKind), single ? 1 : evidence.rootCount); + const center = text.center(quote(single ? nodeDisplayName(first, words.nodeNames) : selectionName(evidence, words)), words.kindName((single ? first.kind : undefined) ?? evidence.selectionKind), single ? 1 : evidence.rootCount); const { direction, types, state } = scopeParts(evidence, words); const reach = evidence.depth === 0 ? text.selectionOnly : text.reach(words.hops(evidence.depth), direction, evidence.direction === 'both'); const side = (name: 'incoming' | 'outgoing'): string => {