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..40f47e1 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,19 @@ 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
+ params = asdict(data)
+ # ask LangPro container to return results in a format suitable for
+ # this application
+ params['format'] = 'annotator'
-
- return {"ok": "true"}
+ try:
+ response = requests.post(
+ url=f"{settings.LANGPRO_URL}/api/prove/",
+ json=params,
+ 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/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.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 @@
-
+ @for(tree of ccgTrees; track $index) {
+
+ }
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..2668054 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 {
@@ -6,9 +6,9 @@ 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";
@Component({
selector: "la-annotation-menu",
@@ -16,9 +16,9 @@ import { AnnotationCommentsComponent } from "../annotation-comments/annotation-c
imports: [
NgbNavModule,
FontAwesomeModule,
- AnnotationParseResultsComponent,
AnnotationTableauComponent,
AnnotationCommentsComponent,
+ ParseSVG
],
templateUrl: "./annotation-menu.component.html",
styleUrl: "./annotation-menu.component.scss",
@@ -29,4 +29,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..6121534
--- /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..48e2529
--- /dev/null
+++ b/frontend/src/app/annotate/parse-tree/parse-svg.component.ts
@@ -0,0 +1,39 @@
+import { Component, ChangeDetectorRef, ElementRef, Input, ViewChild, afterNextRender} from '@angular/core';
+import { CommonModule } from "@angular/common";
+import { Subject } from "rxjs";
+import { CCGTerm, Dimensions } from '@/types';
+import { ParseTree } from './parse-tree.component';
+import { Tree } from "@/tree";
+import svgPanZoom from 'svg-pan-zoom';
+
+@Component({
+ selector: "la-parse-svg",
+ standalone: true,
+ imports: [CommonModule, ParseTree],
+ templateUrl: "./parse-svg.component.svg",
+})
+export class ParseSVG {
+ @ViewChild('svg')
+ svg?: ElementRef;
+
+ treeDimensions$ = new Subject();
+ treeDimensions: Dimensions = {width:0, height: 0};
+
+ constructor(private cdref: ChangeDetectorRef) {
+ afterNextRender(() => {
+ svgPanZoom(this.svg!.nativeElement);
+ });
+ }
+
+ onTreeSize(size: Dimensions) {
+ this.treeDimensions = size;
+ }
+
+ ngAfterViewChecked() {
+ this.treeDimensions$.next(this.treeDimensions);
+ this.cdref.detectChanges();
+ }
+
+ @Input()
+ tree: Tree = Tree.empty();
+}
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..772abb3
--- /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 { CCGTerm, Dimensions } from '@/types';
+
+@Component({
+ selector: "[parse-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: CCGTerm = [];
+
+ @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..bf5d231
--- /dev/null
+++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.svg
@@ -0,0 +1,25 @@
+
+ @if (expanded) {
+
+ }
+ @else {
+ +
+
+ }
+
+ @if (showSubtrees()) {
+ @if (treeNode.children) {
+ @for (child of treeNode.children; track $index; let i = $index) {
+
+ }
+ }
+
+
+ @for (child of treeNode.children; track $index; let i = $index) {
+
+ }
+
+ }
+
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..9b22fad
--- /dev/null
+++ b/frontend/src/app/annotate/parse-tree/parse-tree.component.ts
@@ -0,0 +1,82 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { ParseTerm } from './parse-term.component';
+import { Dimensions, CCGTerm } from '@/types';
+import { TreeNode } from "@/tree";
+
+@Component({
+ selector: "[parse-tree]",
+ standalone: true,
+ imports: [ParseTerm],
+ templateUrl: "./parse-tree.component.svg",
+ styleUrl: "./parse-tree.component.scss",
+})
+export class ParseTree {
+ expanded: boolean = true;
+
+ @Input()
+ treeNode: TreeNode = {value: [], children: []};
+
+ 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.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.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.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
+ `q ${this.subtreePosition(idx)/2} 0 ${this.subtreePosition(idx)/2} ${this.levelHeight/2}`
+ ].join(' ');
+ }
+
+ nodeHeight() {
+ return 40;
+ }
+
+ termClick() {
+ this.expanded = !this.expanded;
+ }
+
+ showSubtrees() {
+ return this.expanded;
+ }
+}
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/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};
diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts
index 1822851..af35f2c 100644
--- a/frontend/src/app/types.ts
+++ b/frontend/src/app/types.ts
@@ -77,3 +77,12 @@ export enum EntailmentLabel {
NEUTRAL = "neutral",
UNKNOWN = "unknown",
}
+
+
+export interface Dimensions {
+ width: number;
+ height: number;
+}
+
+
+export type CCGTerm = string[];
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"