From 67932397d29866a35c1e3425773ff1beda977300 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 13 Aug 2025 10:35:46 +0200 Subject: [PATCH 1/6] preliminary working integration with parser backend --- backend/langpro_annotator/settings.py | 2 + backend/problem/views/parse.py | 27 ++-- .../src/app/annotate/annotate.component.html | 3 +- .../src/app/annotate/annotate.component.ts | 43 +++++- .../annotation-input.component.ts | 11 -- .../annotation-menu.component.html | 3 +- .../annotation-menu.component.ts | 7 +- .../parse-tree/parse-svg.component.svg | 7 + .../parse-tree/parse-svg.component.ts | 31 ++++ .../parse-tree/parse-term.component.svg | 26 ++++ .../parse-tree/parse-term.component.ts | 67 +++++++++ .../parse-tree/parse-tree.component.html | 7 + .../parse-tree/parse-tree.component.scss | 0 .../parse-tree/parse-tree.component.spec.ts | 23 +++ .../parse-tree/parse-tree.component.svg | 33 +++++ .../parse-tree/parse-tree.component.ts | 134 ++++++++++++++++++ .../tableau-svg/tableau-svg.component.ts | 2 +- .../tableau-svg/tableau-term.component.ts | 2 +- .../tableau-svg/tableau-tree.component.ts | 2 +- .../src/app/annotate/tableau-svg/types.ts | 5 - frontend/src/app/services/parse.service.ts | 15 +- frontend/src/app/types.ts | 6 + 22 files changed, 413 insertions(+), 43 deletions(-) create mode 100644 frontend/src/app/annotate/parse-tree/parse-svg.component.svg create mode 100644 frontend/src/app/annotate/parse-tree/parse-svg.component.ts create mode 100644 frontend/src/app/annotate/parse-tree/parse-term.component.svg create mode 100644 frontend/src/app/annotate/parse-tree/parse-term.component.ts create mode 100644 frontend/src/app/annotate/parse-tree/parse-tree.component.html create mode 100644 frontend/src/app/annotate/parse-tree/parse-tree.component.scss create mode 100644 frontend/src/app/annotate/parse-tree/parse-tree.component.spec.ts create mode 100644 frontend/src/app/annotate/parse-tree/parse-tree.component.svg create mode 100644 frontend/src/app/annotate/parse-tree/parse-tree.component.ts delete mode 100644 frontend/src/app/annotate/tableau-svg/types.ts diff --git a/backend/langpro_annotator/settings.py b/backend/langpro_annotator/settings.py index e1a6ed8..6efbd4a 100644 --- a/backend/langpro_annotator/settings.py +++ b/backend/langpro_annotator/settings.py @@ -91,3 +91,5 @@ STATICFILES_DIRS = [] PROXY_FRONTEND = None + +LANGPRO_URL = 'http://localhost:8080' diff --git a/backend/problem/views/parse.py b/backend/problem/views/parse.py index afed05a..cf74639 100644 --- a/backend/problem/views/parse.py +++ b/backend/problem/views/parse.py @@ -1,3 +1,5 @@ +import requests +from django.conf import settings from dataclasses import dataclass, field, asdict from django.http import JsonResponse @@ -67,17 +69,14 @@ def send_to_parser(self, data: ParserInput) -> dict | None: logger.info("Sending to LangPro service:", asdict(data)) - # try: - # response = requests.post( - # url=f"{LANGPRO_URL}/parse", - # json=asdict(data), - # headers={"Content-Type": "application/json"}, - # ) - # response.raise_for_status() - # return response.json() - # except requests.RequestException as e: - # logger.exception(f"Error sending request to LangPro: {e}") - # return None - - - return {"ok": "true"} + try: + response = requests.post( + url=f"{settings.LANGPRO_URL}/api/prove/", + json=asdict(data), + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + logger.exception(f"Error sending request to LangPro: {e}") + raise diff --git a/frontend/src/app/annotate/annotate.component.html b/frontend/src/app/annotate/annotate.component.html index 47faefd..0db7930 100644 --- a/frontend/src/app/annotate/annotate.component.html +++ b/frontend/src/app/annotate/annotate.component.html @@ -8,6 +8,7 @@ - + +
  • diff --git a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts index f0c63cc..983aa6f 100644 --- a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts +++ b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { NgbNavModule } from "@ng-bootstrap/ng-bootstrap"; import { @@ -9,6 +9,7 @@ import { import { AnnotationParseResultsComponent } from "../annotation-parse-results/annotation-parse-results.component"; import { AnnotationTableauComponent } from "../annotation-tableau/annotation-tableau.component"; import { AnnotationCommentsComponent } from "../annotation-comments/annotation-comments.component"; +import { ParseSVG } from "../parse-tree/parse-svg.component"; @Component({ selector: "la-annotation-menu", @@ -19,6 +20,7 @@ import { AnnotationCommentsComponent } from "../annotation-comments/annotation-c AnnotationParseResultsComponent, AnnotationTableauComponent, AnnotationCommentsComponent, + ParseSVG ], templateUrl: "./annotation-menu.component.html", styleUrl: "./annotation-menu.component.scss", @@ -29,4 +31,7 @@ export class AnnotationMenuComponent { public faSquarePollHorizontal = faSquarePollHorizontal; public faTree = faTree; public faPenNib = faPenNib; + + @Input() + public ccgTrees: any[] = []; } diff --git a/frontend/src/app/annotate/parse-tree/parse-svg.component.svg b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg new file mode 100644 index 0000000..053b3bb --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/app/annotate/parse-tree/parse-svg.component.ts b/frontend/src/app/annotate/parse-tree/parse-svg.component.ts new file mode 100644 index 0000000..f2bbd44 --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.ts @@ -0,0 +1,31 @@ +import { Component, ChangeDetectorRef, Input} from '@angular/core'; +import { CommonModule } from "@angular/common"; +import { Subject } from "rxjs"; +import { Dimensions } from '@/types'; +import { ParseTree } from './parse-tree.component'; + +@Component({ + selector: "la-parse-svg", + standalone: true, + imports: [CommonModule, ParseTree], + templateUrl: "./parse-svg.component.svg", +}) +export class ParseSVG { + + treeDimensions$ = new Subject(); + treeDimensions: Dimensions = {width:0, height: 0}; + + constructor(private cdref: ChangeDetectorRef) {} + + onTreeSize(size: Dimensions) { + this.treeDimensions = size; + } + + ngAfterViewChecked() { + this.treeDimensions$.next(this.treeDimensions); + this.cdref.detectChanges(); + } + + @Input() + tree: any = {}; +} diff --git a/frontend/src/app/annotate/parse-tree/parse-term.component.svg b/frontend/src/app/annotate/parse-tree/parse-term.component.svg new file mode 100644 index 0000000..21f9cc9 --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-term.component.svg @@ -0,0 +1,26 @@ + +@if(idx) { + + {{ idx }} +} + +@if(bg) { + +} + + +@for (item of term; track $index) { + {{ item }} +} + + +@if(label) { + + {{ label }} +} + +@if(rule) { +{{ rule }} +} + +< /svg:g> diff --git a/frontend/src/app/annotate/parse-tree/parse-term.component.ts b/frontend/src/app/annotate/parse-tree/parse-term.component.ts new file mode 100644 index 0000000..5abd791 --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-term.component.ts @@ -0,0 +1,67 @@ +import { Component, ElementRef, Input, Output, ViewChild, EventEmitter } from '@angular/core'; +import { Dimensions } from '@/types'; + +@Component({ + selector: "[term]", + standalone: true, + imports: [], + templateUrl: "./parse-term.component.svg", +}) +export class ParseTerm { + @Input() + public idx?: number; + + @Input() + public label?: string; + + /* background color, should probably be replaced by an enum type with the color lookup done elsewhere */ + @Input() + public bg?: string; + + @Input() + public term: string = ''; + + @Input() + public end: boolean = false; + + @Input() + public rule?: string; + + @ViewChild('idxText') + idxText?: ElementRef; + + @ViewChild('termText') + termText?: ElementRef; + + @ViewChild('labelText') + labelText?: ElementRef; + + @Output() + public onSize = new EventEmitter(); + + padding = 2; + height = 20; + + idxW = 0; + termX = 0; + termW = 0; + labelX = 0; + labelW = 0; + totalW = 0; + + calculateWidth() { + return this.termText!.nativeElement.getBBox().width; + } + + ngAfterViewChecked() { + this.idxW = this.idxText ? this.idxText.nativeElement.getComputedTextLength() + this.padding : 0; + this.labelW = this.labelText ? this.labelText.nativeElement.getComputedTextLength() + this.padding : 0; + this.termX = this.idxW; + this.termW = this.termText ? this.calculateWidth() : 0; + this.labelX = this.termX + this.termW + this.padding; + this.totalW = this.termText ? this.labelW + this.idxW + this.calculateWidth() : 0; + + this.onSize.emit({width: this.totalW, height: this.height}); + } + +} diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.html b/frontend/src/app/annotate/parse-tree/parse-tree.component.html new file mode 100644 index 0000000..5f91428 --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.html @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.scss b/frontend/src/app/annotate/parse-tree/parse-tree.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.spec.ts b/frontend/src/app/annotate/parse-tree/parse-tree.component.spec.ts new file mode 100644 index 0000000..975642e --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ParseTree } from './parse-tree.component'; + +describe('ParseTree', () => { + let component: ParseTree; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ParseTree] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ParseTree); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.svg b/frontend/src/app/annotate/parse-tree/parse-tree.component.svg new file mode 100644 index 0000000..e4c71ea --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.svg @@ -0,0 +1,33 @@ + + @for (node of visibleNodes(); track $index; let i = $index) { + + + @if(!expanded[$index]) { + + + + } + @else if (!$last) { + + } + + } + + @if (showSubtrees()) { + @if (tree.subtrees) { + @for (subtree of tree.subtrees; track $index; let i = $index) { + + } + } + + + @for (subtree of tree.subtrees; track $index; let i = $index) { + + } + + } + \ No newline at end of file diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.ts b/frontend/src/app/annotate/parse-tree/parse-tree.component.ts new file mode 100644 index 0000000..f0f40a9 --- /dev/null +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.ts @@ -0,0 +1,134 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { ParseTerm } from './parse-term.component'; +import { Dimensions } from '@/types'; +import { sum } from "@/util"; + +@Component({ + selector: "[parse-tree]", + standalone: true, + imports: [ParseTerm], + templateUrl: "./parse-tree.component.svg", + styleUrl: "./parse-tree.component.scss", +}) +export class ParseTree { + expanded: boolean[] = []; + + _tree: any; + @Input() + get tree(): any { + return this._tree; + } + + set tree(value: any) { + if (value.children) { + // this is a temporary translation until the tree code can be refactored: + let translated = + (function translate(p: any) { + if (!p) return; + let out: any = {}; + if (Array.isArray(p.node)) { + out.nodes = [{term: p.node}]; + } + else { + out.nodes = [{term: [p.node]}]; + } + out.subtrees = p.children ? p.children.map(translate) : []; + return out; + })(value); + this._tree = translated; + } + else { + this._tree = value; + } + + this.expanded = [true]; + } + + levelHeight = 40; + + /* Width of the tree node, has to be determined dynamically via onSize events from terms */ + width = 0; + /* Height isn't stored in a member variable, because it can be computed as needed */ + + /* Dimensions of the biggest subtree. + Width is used to align all subtrees. + Height is used to determine the overall height of the tree. */ + subWidth = 0; + subHeight = 0; + + @Output() + public onSize = new EventEmitter(); + + updateDimensions(size: Dimensions) { + this.width = Math.max(this.width, size.width!); + this.emitSize(); + } + + /* keep track of the current node's biggest subtree using onSize events */ + updateSubDimensions(size: Dimensions) { + this.subWidth = Math.max(this.subWidth, size.width!); + this.subHeight = Math.max(this.subHeight, size.height!); + this.emitSize(); + } + + emitSize() { + this.onSize.emit({ + width: Math.max(this.subWidth * (this.tree.subtrees?.length ?? 0), this.width), + height: this.subHeight + this.totalNodeHeight() + (this.tree.subtrees?.length ? this.levelHeight : 0) + }); + } + + /* Determines the X coordinate of where subtree of index `idx` should be drawn */ + subtreePosition(idx: number) { + let widthWithPadding = 1.15 * this.subWidth; + return widthWithPadding * idx - (widthWithPadding / 2) * (this.tree.subtrees.length - 1); + } + + /* Generates a path definition for linking the end of the current node to subtree index `idx` */ + subtreeLinkPath(idx: number) { + return [ + // move to end of current node + `M 0 ${this.totalNodeHeight() - 15 }`, + // curve to half-way to subtree + `q 0 ${this.levelHeight/2} ${this.subtreePosition(idx)/2 } ${this.levelHeight/2}`, + // curve from half-way to subtree, to subtree position + `q ${this.subtreePosition(idx)/2} 0 ${this.subtreePosition(idx)/2} ${this.levelHeight/2}` + ].join(' '); + } + + nodeHeight(node: any) { + // note that in this case height includes bottom padding for the node + return node.rule ? 60 : 40; + } + + totalNodeHeight() { + return sum(this.tree.nodes.map(this.nodeHeight)); + } + + nodeY(idx: number) { + // y position of a given node is the sum of heights of all preceeding nodes + return sum(this.tree.nodes.slice(0, idx).map(this.nodeHeight)); + } + + termClick(idx: number) { + // the last node can't be collapsed + // unless there are subtrees + if (idx < this.expanded.length - 1 || this.tree.subtrees) { + this.expanded[idx] = !this.expanded[idx]; + } + } + + visibleNodes() { + const firstCollpased = this.expanded.indexOf(false); + if (firstCollpased == -1) { + return this.tree.nodes; + } + return this.tree.nodes.slice(0, firstCollpased + 1); + } + + showSubtrees() { + // if any of the current level nodes is collapsed, this also means + // we shouldn't render any subtrees + return this.expanded.indexOf(false) == -1; + } +} diff --git a/frontend/src/app/annotate/tableau-svg/tableau-svg.component.ts b/frontend/src/app/annotate/tableau-svg/tableau-svg.component.ts index f69d8c5..8a84fb3 100644 --- a/frontend/src/app/annotate/tableau-svg/tableau-svg.component.ts +++ b/frontend/src/app/annotate/tableau-svg/tableau-svg.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectorRef} from '@angular/core'; import { CommonModule } from "@angular/common"; import { Subject } from "rxjs"; -import { Dimensions } from './types'; +import { Dimensions } from '@/types'; import { TableauTree } from './tableau-tree.component'; @Component({ diff --git a/frontend/src/app/annotate/tableau-svg/tableau-term.component.ts b/frontend/src/app/annotate/tableau-svg/tableau-term.component.ts index 6a03d79..616a0ba 100644 --- a/frontend/src/app/annotate/tableau-svg/tableau-term.component.ts +++ b/frontend/src/app/annotate/tableau-svg/tableau-term.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, Input, Output, ViewChild, EventEmitter } from '@angular/core'; -import { Dimensions } from './types'; +import { Dimensions } from '@/types'; @Component({ selector: "[term]", diff --git a/frontend/src/app/annotate/tableau-svg/tableau-tree.component.ts b/frontend/src/app/annotate/tableau-svg/tableau-tree.component.ts index 1148526..e2e7ef7 100644 --- a/frontend/src/app/annotate/tableau-svg/tableau-tree.component.ts +++ b/frontend/src/app/annotate/tableau-svg/tableau-tree.component.ts @@ -1,6 +1,6 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { TableauTerm } from './tableau-term.component'; -import { Dimensions } from './types'; +import { Dimensions } from '@/types'; import { sum } from "@/util"; @Component({ diff --git a/frontend/src/app/annotate/tableau-svg/types.ts b/frontend/src/app/annotate/tableau-svg/types.ts deleted file mode 100644 index d416515..0000000 --- a/frontend/src/app/annotate/tableau-svg/types.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface Dimensions { - width: number; - height: number; -} diff --git a/frontend/src/app/services/parse.service.ts b/frontend/src/app/services/parse.service.ts index 3605f6d..53e5ca5 100644 --- a/frontend/src/app/services/parse.service.ts +++ b/frontend/src/app/services/parse.service.ts @@ -2,7 +2,9 @@ import { ParseInput } from '@/annotate/annotation-input/annotation-input.compone import { ProblemResponse } from '@/types'; import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { Subject, switchMap, catchError, of } from 'rxjs'; +import { Subject, switchMap, catchError, of, first, Observer } from 'rxjs'; + +export type ParseResponse = any @Injectable({ providedIn: 'root' @@ -10,11 +12,11 @@ import { Subject, switchMap, catchError, of } from 'rxjs'; export class ParseService { private http = inject(HttpClient); - public submit = new Subject(); + private submit = new Subject(); - public parse$ = this.submit.pipe( + private parse$ = this.submit.pipe( switchMap((form) => - this.http.post("/api/problem/parse", form).pipe( + this.http.post("/api/problem/parse", form).pipe( catchError((error) => { console.error(`Error parsing problem:`, error); return of(null); @@ -22,4 +24,9 @@ export class ParseService { ) ) ); + + public startParse(input: ParseInput, handler: (response: ParseResponse) => void) { + this.parse$.pipe(first()).subscribe(handler); + this.submit.next(input); + } } diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts index 1822851..9999174 100644 --- a/frontend/src/app/types.ts +++ b/frontend/src/app/types.ts @@ -77,3 +77,9 @@ export enum EntailmentLabel { NEUTRAL = "neutral", UNKNOWN = "unknown", } + + +export interface Dimensions { + width: number; + height: number; +} From a1f8b29ccc1c77fcd27ee9b3e27b3f2036f8d948 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 17 Sep 2025 15:13:20 +0200 Subject: [PATCH 2/6] pan/zoom support for svg tree --- frontend/angular.json | 2 +- frontend/package.json | 3 ++- frontend/src/app/annotate/annotate.component.ts | 1 - .../app/annotate/parse-tree/parse-svg.component.svg | 2 +- .../app/annotate/parse-tree/parse-svg.component.ts | 11 +++++++++-- frontend/yarn.lock | 4 ++++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/angular.json b/frontend/angular.json index 6fbc171..694c05d 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -47,7 +47,7 @@ ], "scripts": [], "server": "src/main.server.ts", - "prerender": true, + "prerender": false, "ssr": false, "baseHref": "/" }, diff --git a/frontend/package.json b/frontend/package.json index 3fb2ed1..2d74adc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "colors": "^1.4.0", "express": "^4.18.2", "rxjs": "~7.8.0", + "svg-pan-zoom": "bumbu/svg-pan-zoom", "tslib": "^2.3.0", "zone.js": "~0.15.1" }, @@ -57,4 +58,4 @@ "ng-extract-i18n-merge": "^2.12.0", "typescript": "~5.8.3" } -} \ No newline at end of file +} diff --git a/frontend/src/app/annotate/annotate.component.ts b/frontend/src/app/annotate/annotate.component.ts index 4d711c2..d916fcc 100644 --- a/frontend/src/app/annotate/annotate.component.ts +++ b/frontend/src/app/annotate/annotate.component.ts @@ -50,7 +50,6 @@ export class AnnotateComponent implements OnInit { } startParse() { - // hardcoded data for now let input: ParseInput = { premises: this.problem?.problem?.premises!, hypothesis: this.problem?.problem?.hypothesis!, diff --git a/frontend/src/app/annotate/parse-tree/parse-svg.component.svg b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg index 053b3bb..0bbc6d0 100644 --- a/frontend/src/app/annotate/parse-tree/parse-svg.component.svg +++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg @@ -1,4 +1,4 @@ - ; treeDimensions$ = new Subject(); treeDimensions: Dimensions = {width:0, height: 0}; - constructor(private cdref: ChangeDetectorRef) {} + constructor(private cdref: ChangeDetectorRef) { + afterNextRender(() => { + svgPanZoom(this.svg!.nativeElement); + }); + } onTreeSize(size: Dimensions) { this.treeDimensions = size; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index da27d19..24bf676 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6764,6 +6764,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-pan-zoom@bumbu/svg-pan-zoom: + version "3.6.2" + resolved "https://codeload.github.com/bumbu/svg-pan-zoom/tar.gz/aaa68d186abab5d782191b66d2582592fe5d3c13" + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" From 1af367cbe1e53b7d1b71358ae8d165364a76f3cf Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 1 Oct 2025 16:22:08 +0200 Subject: [PATCH 3/6] added format param --- backend/problem/views/parse.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/problem/views/parse.py b/backend/problem/views/parse.py index cf74639..ed0893f 100644 --- a/backend/problem/views/parse.py +++ b/backend/problem/views/parse.py @@ -69,10 +69,13 @@ def send_to_parser(self, data: ParserInput) -> dict | None: logger.info("Sending to LangPro service:", asdict(data)) + params = asdict(data) + params['format'] = 'annotator' + try: response = requests.post( url=f"{settings.LANGPRO_URL}/api/prove/", - json=asdict(data), + json=params, headers={"Content-Type": "application/json"}, ) response.raise_for_status() From 19e7f5e5e15cc5067f76a5b9dc6b91ba9763ddd0 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 1 Oct 2025 16:32:36 +0200 Subject: [PATCH 4/6] multiple ccg trees --- .../annotate/annotation-menu/annotation-menu.component.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.html b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.html index 00117ba..d811962 100644 --- a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.html +++ b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.html @@ -9,8 +9,9 @@ CCG / LLF - - + @for(tree of ccgTrees; track $index) { + + }
  • From 2174cd3a0c2bb4ab5db8778c03d2d3ba07431af5 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 9 Oct 2025 11:34:11 +0200 Subject: [PATCH 5/6] refactored parse tree and data types --- backend/problem/views/parse.py | 2 + .../src/app/annotate/annotate.component.ts | 5 +- .../annotation-menu.component.ts | 2 - .../parse-tree/parse-svg.component.svg | 2 +- .../parse-tree/parse-svg.component.ts | 5 +- .../parse-tree/parse-term.component.ts | 6 +- .../parse-tree/parse-tree.component.svg | 34 ++++---- .../parse-tree/parse-tree.component.ts | 78 ++++--------------- frontend/src/app/types.ts | 3 + 9 files changed, 41 insertions(+), 96 deletions(-) diff --git a/backend/problem/views/parse.py b/backend/problem/views/parse.py index ed0893f..40f47e1 100644 --- a/backend/problem/views/parse.py +++ b/backend/problem/views/parse.py @@ -70,6 +70,8 @@ def send_to_parser(self, data: ParserInput) -> dict | None: logger.info("Sending to LangPro service:", asdict(data)) params = asdict(data) + # ask LangPro container to return results in a format suitable for + # this application params['format'] = 'annotator' try: diff --git a/frontend/src/app/annotate/annotate.component.ts b/frontend/src/app/annotate/annotate.component.ts index d916fcc..8628107 100644 --- a/frontend/src/app/annotate/annotate.component.ts +++ b/frontend/src/app/annotate/annotate.component.ts @@ -9,6 +9,7 @@ import { ParseResponse, ParseService } from "@/services/parse.service"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ProblemResponse } from "@/types"; import { ProblemService } from "@/services/problem.service"; +import { Tree } from "@/tree"; @Component({ selector: "la-annotate", @@ -29,7 +30,7 @@ export class AnnotateComponent implements OnInit { private parseService = inject(ParseService); private problemService = inject(ProblemService); - public ccgTrees: any[] = []; + public ccgTrees: Tree[] = []; private problem: ProblemResponse | null = null; @@ -46,7 +47,7 @@ export class AnnotateComponent implements OnInit { onParse(response: ParseResponse) { console.log("Parse response:", response); - this.ccgTrees = response!.data.ccg_trees; + this.ccgTrees = response!.data.ccg_trees.map((tree: any) => new Tree(tree)); } startParse() { diff --git a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts index 983aa6f..2668054 100644 --- a/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts +++ b/frontend/src/app/annotate/annotation-menu/annotation-menu.component.ts @@ -6,7 +6,6 @@ import { faTree, faPenNib, } from "@fortawesome/free-solid-svg-icons"; -import { AnnotationParseResultsComponent } from "../annotation-parse-results/annotation-parse-results.component"; import { AnnotationTableauComponent } from "../annotation-tableau/annotation-tableau.component"; import { AnnotationCommentsComponent } from "../annotation-comments/annotation-comments.component"; import { ParseSVG } from "../parse-tree/parse-svg.component"; @@ -17,7 +16,6 @@ import { ParseSVG } from "../parse-tree/parse-svg.component"; imports: [ NgbNavModule, FontAwesomeModule, - AnnotationParseResultsComponent, AnnotationTableauComponent, AnnotationCommentsComponent, ParseSVG diff --git a/frontend/src/app/annotate/parse-tree/parse-svg.component.svg b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg index 0bbc6d0..6121534 100644 --- a/frontend/src/app/annotate/parse-tree/parse-svg.component.svg +++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.svg @@ -1,7 +1,7 @@ - diff --git a/frontend/src/app/annotate/parse-tree/parse-svg.component.ts b/frontend/src/app/annotate/parse-tree/parse-svg.component.ts index 53032db..48e2529 100644 --- a/frontend/src/app/annotate/parse-tree/parse-svg.component.ts +++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.ts @@ -1,8 +1,9 @@ import { Component, ChangeDetectorRef, ElementRef, Input, ViewChild, afterNextRender} from '@angular/core'; import { CommonModule } from "@angular/common"; import { Subject } from "rxjs"; -import { Dimensions } from '@/types'; +import { CCGTerm, Dimensions } from '@/types'; import { ParseTree } from './parse-tree.component'; +import { Tree } from "@/tree"; import svgPanZoom from 'svg-pan-zoom'; @Component({ @@ -34,5 +35,5 @@ export class ParseSVG { } @Input() - tree: any = {}; + tree: Tree = Tree.empty(); } diff --git a/frontend/src/app/annotate/parse-tree/parse-term.component.ts b/frontend/src/app/annotate/parse-tree/parse-term.component.ts index 5abd791..772abb3 100644 --- a/frontend/src/app/annotate/parse-tree/parse-term.component.ts +++ b/frontend/src/app/annotate/parse-tree/parse-term.component.ts @@ -1,8 +1,8 @@ import { Component, ElementRef, Input, Output, ViewChild, EventEmitter } from '@angular/core'; -import { Dimensions } from '@/types'; +import { CCGTerm, Dimensions } from '@/types'; @Component({ - selector: "[term]", + selector: "[parse-term]", standalone: true, imports: [], templateUrl: "./parse-term.component.svg", @@ -19,7 +19,7 @@ export class ParseTerm { public bg?: string; @Input() - public term: string = ''; + public term: CCGTerm = []; @Input() public end: boolean = false; diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.svg b/frontend/src/app/annotate/parse-tree/parse-tree.component.svg index e4c71ea..bf5d231 100644 --- a/frontend/src/app/annotate/parse-tree/parse-tree.component.svg +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.svg @@ -1,33 +1,25 @@ - @for (node of visibleNodes(); track $index; let i = $index) { - - - @if(!expanded[$index]) { - + - - } - @else if (!$last) { - - } - + @if (expanded) { + + } + @else { + + + } @if (showSubtrees()) { - @if (tree.subtrees) { - @for (subtree of tree.subtrees; track $index; let i = $index) { + @if (treeNode.children) { + @for (child of treeNode.children; track $index; let i = $index) { } } - - @for (subtree of tree.subtrees; track $index; let i = $index) { - + @for (child of treeNode.children; track $index; let i = $index) { + } } - \ No newline at end of file + diff --git a/frontend/src/app/annotate/parse-tree/parse-tree.component.ts b/frontend/src/app/annotate/parse-tree/parse-tree.component.ts index f0f40a9..9b22fad 100644 --- a/frontend/src/app/annotate/parse-tree/parse-tree.component.ts +++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.ts @@ -1,7 +1,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { ParseTerm } from './parse-term.component'; -import { Dimensions } from '@/types'; -import { sum } from "@/util"; +import { Dimensions, CCGTerm } from '@/types'; +import { TreeNode } from "@/tree"; @Component({ selector: "[parse-tree]", @@ -11,38 +11,10 @@ import { sum } from "@/util"; styleUrl: "./parse-tree.component.scss", }) export class ParseTree { - expanded: boolean[] = []; + expanded: boolean = true; - _tree: any; @Input() - get tree(): any { - return this._tree; - } - - set tree(value: any) { - if (value.children) { - // this is a temporary translation until the tree code can be refactored: - let translated = - (function translate(p: any) { - if (!p) return; - let out: any = {}; - if (Array.isArray(p.node)) { - out.nodes = [{term: p.node}]; - } - else { - out.nodes = [{term: [p.node]}]; - } - out.subtrees = p.children ? p.children.map(translate) : []; - return out; - })(value); - this._tree = translated; - } - else { - this._tree = value; - } - - this.expanded = [true]; - } + treeNode: TreeNode = {value: [], children: []}; levelHeight = 40; @@ -73,22 +45,22 @@ export class ParseTree { emitSize() { this.onSize.emit({ - width: Math.max(this.subWidth * (this.tree.subtrees?.length ?? 0), this.width), - height: this.subHeight + this.totalNodeHeight() + (this.tree.subtrees?.length ? this.levelHeight : 0) + width: Math.max(this.subWidth * (this.treeNode?.children?.length ?? 0), this.width), + height: this.subHeight + this.nodeHeight() + (this.treeNode?.children?.length ? this.levelHeight : 0) }); } /* Determines the X coordinate of where subtree of index `idx` should be drawn */ subtreePosition(idx: number) { let widthWithPadding = 1.15 * this.subWidth; - return widthWithPadding * idx - (widthWithPadding / 2) * (this.tree.subtrees.length - 1); + return widthWithPadding * idx - (widthWithPadding / 2) * (this.treeNode.children.length - 1); } /* Generates a path definition for linking the end of the current node to subtree index `idx` */ subtreeLinkPath(idx: number) { return [ // move to end of current node - `M 0 ${this.totalNodeHeight() - 15 }`, + `M 0 ${this.nodeHeight() - 15 }`, // curve to half-way to subtree `q 0 ${this.levelHeight/2} ${this.subtreePosition(idx)/2 } ${this.levelHeight/2}`, // curve from half-way to subtree, to subtree position @@ -96,39 +68,15 @@ export class ParseTree { ].join(' '); } - nodeHeight(node: any) { - // note that in this case height includes bottom padding for the node - return node.rule ? 60 : 40; - } - - totalNodeHeight() { - return sum(this.tree.nodes.map(this.nodeHeight)); - } - - nodeY(idx: number) { - // y position of a given node is the sum of heights of all preceeding nodes - return sum(this.tree.nodes.slice(0, idx).map(this.nodeHeight)); - } - - termClick(idx: number) { - // the last node can't be collapsed - // unless there are subtrees - if (idx < this.expanded.length - 1 || this.tree.subtrees) { - this.expanded[idx] = !this.expanded[idx]; - } + nodeHeight() { + return 40; } - visibleNodes() { - const firstCollpased = this.expanded.indexOf(false); - if (firstCollpased == -1) { - return this.tree.nodes; - } - return this.tree.nodes.slice(0, firstCollpased + 1); + termClick() { + this.expanded = !this.expanded; } showSubtrees() { - // if any of the current level nodes is collapsed, this also means - // we shouldn't render any subtrees - return this.expanded.indexOf(false) == -1; + return this.expanded; } } diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts index 9999174..af35f2c 100644 --- a/frontend/src/app/types.ts +++ b/frontend/src/app/types.ts @@ -83,3 +83,6 @@ export interface Dimensions { width: number; height: number; } + + +export type CCGTerm = string[]; From e57c1f3a6a8f12aa265ce2f76463a416d2f81f47 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 9 Feb 2026 10:10:00 +0100 Subject: [PATCH 6/6] added missing tree.ts to repo --- frontend/src/app/tree.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 frontend/src/app/tree.ts diff --git a/frontend/src/app/tree.ts b/frontend/src/app/tree.ts new file mode 100644 index 0000000..6d70ad2 --- /dev/null +++ b/frontend/src/app/tree.ts @@ -0,0 +1,31 @@ + +interface TreeNode { + value: T; + children: TreeNode[]; +} + +class Tree { + _root?: TreeNode; + + get root(): TreeNode { + if (!this._root) { + throw new Error("Tree is empty") + } + return this._root; + } + + constructor(root?: TreeNode) { + this._root = root; + } + + static empty(): Tree { + return new Tree(); + } + + static fromJSON(json: any): Tree { + // there's no validation of the content + return new Tree(json); + } +} + +export {Tree, TreeNode};