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"