Compare commits
7 Commits
konva-docu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
806f4a28e4 | ||
|
|
095c1ca8ec | ||
|
|
7dd6de064f | ||
|
|
ee5ac6ea69 | ||
|
|
f129a9cb13 | ||
|
|
917662e9ba | ||
|
|
1631271b93 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -3,10 +3,15 @@
|
|||||||
"*.css": "tailwindcss"
|
"*.css": "tailwindcss"
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"consts",
|
||||||
"headlessui",
|
"headlessui",
|
||||||
"heroicons",
|
"heroicons",
|
||||||
"konva",
|
"konva",
|
||||||
|
"libretranslate",
|
||||||
|
"reduxjs",
|
||||||
|
"tailwindcss",
|
||||||
"Tesseract",
|
"Tesseract",
|
||||||
|
"Textualize",
|
||||||
"wailsjs"
|
"wailsjs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
13
README.md
13
README.md
@ -1 +1,14 @@
|
|||||||
# Textualize
|
# Textualize
|
||||||
|
|
||||||
|
Textualize is a desktop application designed to process your photos or scans of physical text documents, convert them into textual data and translate them.
|
||||||
|
|
||||||
|
Textualize comes with an interface to edit, modify, and manage your projects, making it a powerful tool to work on entire volumes as well as single page documents.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This project is still under development. Currently on English, classic Hebrew script, and Rashi script are available options for textualizing in the GUI.
|
||||||
|
|
||||||
|
Context linking for translations and translation output are still very much buggy and not accessible through the GUI
|
||||||
|
|
||||||
|
Built with Wails
|
||||||
|
Tested only on MacOS
|
||||||
147
core/ContextGroup/ContextGroupCollection.go
Normal file
147
core/ContextGroup/ContextGroupCollection.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package contextGroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"textualize/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextGroupCollection struct {
|
||||||
|
Groups []entities.LinkedAreaList
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextGroupCollectionInstance *ContextGroupCollection
|
||||||
|
|
||||||
|
func GetContextGroupCollection() *ContextGroupCollection {
|
||||||
|
if contextGroupCollectionInstance == nil {
|
||||||
|
contextGroupCollectionInstance = &ContextGroupCollection{}
|
||||||
|
}
|
||||||
|
return contextGroupCollectionInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetContextGroupCollection(collection ContextGroupCollection) *ContextGroupCollection {
|
||||||
|
contextGroupCollectionInstance = &collection
|
||||||
|
return contextGroupCollectionInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetContextGroupCollectionBySerialized(serialized []entities.SerializedLinkedProcessedArea) *ContextGroupCollection {
|
||||||
|
newInstance := ContextGroupCollection{}
|
||||||
|
|
||||||
|
newInstance.Groups = append(newInstance.Groups, entities.DeserializeLinkedAreaList(serialized))
|
||||||
|
|
||||||
|
SetContextGroupCollection(newInstance)
|
||||||
|
return &newInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) DoesGroupExistBetweenProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
|
||||||
|
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId)
|
||||||
|
descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantAreaId)
|
||||||
|
|
||||||
|
isAncestorInAnyInGroup := ancestorGroup != nil
|
||||||
|
isDescendantInAnyInGroup := descendantGroup != nil
|
||||||
|
areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup
|
||||||
|
areBothInSameGroup := false
|
||||||
|
if areBothInAnyInGroup {
|
||||||
|
areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return areBothInSameGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) DisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
|
||||||
|
doesConnectionExist := collection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId)
|
||||||
|
|
||||||
|
if !doesConnectionExist {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorAreaId)
|
||||||
|
|
||||||
|
wasRemoved := false
|
||||||
|
for i, group := range collection.Groups {
|
||||||
|
if group.Id == ancestorGroup.Id {
|
||||||
|
collection.Groups = append(collection.Groups[:i], collection.Groups[i+1:]...)
|
||||||
|
wasRemoved = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wasRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) FindGroupById(id string) (*entities.LinkedAreaList, error) {
|
||||||
|
found := false
|
||||||
|
var foundGroup *entities.LinkedAreaList = nil
|
||||||
|
for _, group := range collection.Groups {
|
||||||
|
if group.Id == id {
|
||||||
|
found = true
|
||||||
|
foundGroup = &group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("ContextGroupCollection.FindGroupById: Group with id %s not found", id)
|
||||||
|
}
|
||||||
|
return foundGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) FindGroupByLinkedProcessedAreaId(id string) (*entities.LinkedAreaList, error) {
|
||||||
|
found := false
|
||||||
|
var foundGroup *entities.LinkedAreaList = nil
|
||||||
|
for _, group := range collection.Groups {
|
||||||
|
for n := group.First(); n != nil && !found; n = n.GetNext() {
|
||||||
|
if n.Area.Id == id {
|
||||||
|
found = true
|
||||||
|
foundGroup = &group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("ContextGroupCollection.FindGroupByLinkedProcessedAreaId: Group with LinkedProcessedArea.Id %s not found", id)
|
||||||
|
}
|
||||||
|
return foundGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) ConnectProcessedAreas(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) bool {
|
||||||
|
ancestorGroup, _ := collection.FindGroupByLinkedProcessedAreaId(ancestorNode.Id)
|
||||||
|
descendantGroup, _ := collection.FindGroupByLinkedProcessedAreaId(descendantNode.Id)
|
||||||
|
|
||||||
|
isAncestorInAnyInGroup := ancestorGroup != nil
|
||||||
|
isDescendantInAnyInGroup := descendantGroup != nil
|
||||||
|
isEitherInAnyInGroup := isAncestorInAnyInGroup || isDescendantInAnyInGroup
|
||||||
|
areBothInAnyInGroup := isAncestorInAnyInGroup && isDescendantInAnyInGroup
|
||||||
|
areBothInSameGroup := false
|
||||||
|
if areBothInAnyInGroup {
|
||||||
|
areBothInSameGroup = ancestorGroup.Id == descendantGroup.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
if areBothInSameGroup {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isEitherInAnyInGroup {
|
||||||
|
collection.createNewGroupAndConnectNodes(ancestorNode, descendantNode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAncestorInAnyInGroup && !isDescendantInAnyInGroup {
|
||||||
|
ancestorGroup.InsertAfter(ancestorNode.Id, descendantNode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAncestorInAnyInGroup && isDescendantInAnyInGroup {
|
||||||
|
descendantGroup.InsertBefore(descendantNode.Id, ancestorNode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (collection *ContextGroupCollection) createNewGroupAndConnectNodes(ancestorNode entities.ProcessedArea, descendantNode entities.ProcessedArea) {
|
||||||
|
newGroup := entities.LinkedAreaList{
|
||||||
|
Id: ancestorNode.Id,
|
||||||
|
DocumentId: ancestorNode.DocumentId,
|
||||||
|
Head: &entities.LinkedProcessedArea{Area: ancestorNode},
|
||||||
|
Tail: &entities.LinkedProcessedArea{Area: descendantNode},
|
||||||
|
}
|
||||||
|
newGroup.Head.Next = newGroup.Tail
|
||||||
|
newGroup.Tail.Previous = newGroup.Head
|
||||||
|
collection.Groups = append(collection.Groups, newGroup)
|
||||||
|
}
|
||||||
BIN
docs/assets/overviewScreenshot.png
Normal file
BIN
docs/assets/overviewScreenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
173
entities/ContextGroup.go
Normal file
173
entities/ContextGroup.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndependentTranslatedWord struct {
|
||||||
|
Id string
|
||||||
|
ProcessedWordId string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedProcessedArea struct {
|
||||||
|
Area ProcessedArea
|
||||||
|
Previous *LinkedProcessedArea
|
||||||
|
Next *LinkedProcessedArea
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerializedLinkedProcessedArea struct {
|
||||||
|
AreaId string `json:"areaId"`
|
||||||
|
PreviousId string `json:"previousId"`
|
||||||
|
NextId string `json:"nextId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextGroupCollection struct {
|
||||||
|
Groups []LinkedAreaList
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedAreaList struct {
|
||||||
|
Id string
|
||||||
|
DocumentId string
|
||||||
|
TranslationText string
|
||||||
|
Head *LinkedProcessedArea
|
||||||
|
Tail *LinkedProcessedArea
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) First() *LinkedProcessedArea {
|
||||||
|
return l.Head
|
||||||
|
}
|
||||||
|
|
||||||
|
func (linkedProcessedWord *LinkedProcessedArea) GetNext() *LinkedProcessedArea {
|
||||||
|
return linkedProcessedWord.Next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (linkedProcessedWord *LinkedProcessedArea) GetPrevious() *LinkedProcessedArea {
|
||||||
|
return linkedProcessedWord.Previous
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new node with value
|
||||||
|
func (l *LinkedAreaList) Push(processedArea ProcessedArea) *LinkedAreaList {
|
||||||
|
n := &LinkedProcessedArea{Area: processedArea}
|
||||||
|
if l.Head == nil {
|
||||||
|
l.Head = n // First node
|
||||||
|
} else {
|
||||||
|
l.Tail.Next = n // Add after prev last node
|
||||||
|
n.Previous = l.Tail // Link back to prev last node
|
||||||
|
}
|
||||||
|
l.Tail = n // reset tail to newly added node
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) Find(id string) *LinkedProcessedArea {
|
||||||
|
found := false
|
||||||
|
var ret *LinkedProcessedArea = nil
|
||||||
|
for n := l.First(); n != nil && !found; n = n.GetNext() {
|
||||||
|
if n.Area.Id == id {
|
||||||
|
found = true
|
||||||
|
ret = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) Delete(id string) bool {
|
||||||
|
success := false
|
||||||
|
node2del := l.Find(id)
|
||||||
|
if node2del != nil {
|
||||||
|
fmt.Println("Delete - FOUND: ", id)
|
||||||
|
prev_node := node2del.Previous
|
||||||
|
next_node := node2del.Next
|
||||||
|
// Remove this node
|
||||||
|
prev_node.Next = node2del.Next
|
||||||
|
next_node.Previous = node2del.Previous
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
var errEmpty = errors.New("ERROR - List is empty")
|
||||||
|
|
||||||
|
// Pop last item from list
|
||||||
|
func (l *LinkedAreaList) Pop() (processedArea ProcessedArea, err error) {
|
||||||
|
if l.Tail == nil {
|
||||||
|
err = errEmpty
|
||||||
|
} else {
|
||||||
|
processedArea = l.Tail.Area
|
||||||
|
l.Tail = l.Tail.Previous
|
||||||
|
if l.Tail == nil {
|
||||||
|
l.Head = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processedArea, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) InsertAfter(id string, processedArea ProcessedArea) bool {
|
||||||
|
found := false
|
||||||
|
for n := l.First(); n != nil && !found; n = n.GetNext() {
|
||||||
|
if n.Area.Id == id {
|
||||||
|
found = true
|
||||||
|
newNode := &LinkedProcessedArea{Area: processedArea}
|
||||||
|
newNode.Next = n.Next
|
||||||
|
newNode.Previous = n
|
||||||
|
n.Next = newNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) InsertBefore(id string, processedArea ProcessedArea) bool {
|
||||||
|
found := false
|
||||||
|
for n := l.First(); n != nil && !found; n = n.GetNext() {
|
||||||
|
if n.Area.Id == id {
|
||||||
|
found = true
|
||||||
|
newNode := &LinkedProcessedArea{Area: processedArea}
|
||||||
|
newNode.Next = n
|
||||||
|
newNode.Previous = n.Previous
|
||||||
|
n.Previous = newNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LinkedAreaList) Serialize() []SerializedLinkedProcessedArea {
|
||||||
|
var serialized []SerializedLinkedProcessedArea
|
||||||
|
for n := l.First(); n != nil; n = n.GetNext() {
|
||||||
|
areaId := n.Area.Id
|
||||||
|
previousId := ""
|
||||||
|
if n.Previous != nil {
|
||||||
|
previousId = n.Previous.Area.Id
|
||||||
|
}
|
||||||
|
nextId := ""
|
||||||
|
if n.Next != nil {
|
||||||
|
nextId = n.Next.Area.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized = append(serialized, SerializedLinkedProcessedArea{
|
||||||
|
AreaId: areaId,
|
||||||
|
PreviousId: previousId,
|
||||||
|
NextId: nextId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeserializeLinkedAreaList(serialized []SerializedLinkedProcessedArea) LinkedAreaList {
|
||||||
|
linkedAreaList := LinkedAreaList{}
|
||||||
|
for _, serializedLinkedProcessedArea := range serialized {
|
||||||
|
linkedAreaList.Push(ProcessedArea{
|
||||||
|
Id: serializedLinkedProcessedArea.AreaId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, serializedLinkedProcessedArea := range serialized {
|
||||||
|
linkedProcessedArea := linkedAreaList.Find(serializedLinkedProcessedArea.AreaId)
|
||||||
|
if serializedLinkedProcessedArea.PreviousId != "" {
|
||||||
|
linkedProcessedArea.Previous = linkedAreaList.Find(serializedLinkedProcessedArea.PreviousId)
|
||||||
|
}
|
||||||
|
if serializedLinkedProcessedArea.NextId != "" {
|
||||||
|
linkedProcessedArea.Next = linkedAreaList.Find(serializedLinkedProcessedArea.NextId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return linkedAreaList
|
||||||
|
}
|
||||||
@ -23,5 +23,6 @@ type Area struct {
|
|||||||
EndX int `json:"endX"`
|
EndX int `json:"endX"`
|
||||||
EndY int `json:"endY"`
|
EndY int `json:"endY"`
|
||||||
Language Language `json:"language"`
|
Language Language `json:"language"`
|
||||||
|
TranslateLanguage Language `json:"translateLanguage"`
|
||||||
Order int `json:"order"`
|
Order int `json:"order"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type ProcessedSymbol struct {
|
|||||||
|
|
||||||
type ProcessedWord struct {
|
type ProcessedWord struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
AreaId string `json:"areaId"`
|
||||||
FullText string `json:"fullText"`
|
FullText string `json:"fullText"`
|
||||||
Symbols []ProcessedSymbol `json:"symbols"`
|
Symbols []ProcessedSymbol `json:"symbols"`
|
||||||
Confidence float32 `json:"confidence"`
|
Confidence float32 `json:"confidence"`
|
||||||
|
|||||||
@ -1,49 +1,34 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import Konva from 'konva'
|
||||||
import { Group, Rect } from 'react-konva'
|
import { Group, Rect } from 'react-konva'
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node'
|
||||||
import { entities } from '../../wailsjs/wailsjs/go/models'
|
import { entities } from '../../wailsjs/wailsjs/go/models'
|
||||||
import { useProject } from '../../context/Project/provider'
|
import { useProject } from '../../context/Project/provider'
|
||||||
import { KonvaEventObject } from 'konva/lib/Node'
|
|
||||||
import Konva from 'konva'
|
|
||||||
import AreaContextMenu from './AreaContextMenu'
|
import AreaContextMenu from './AreaContextMenu'
|
||||||
|
import { RootState } from '../../redux/store'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
area: entities.Area,
|
area: entities.Area,
|
||||||
scale: number,
|
|
||||||
setHoveredOverAreaIds: Function
|
|
||||||
setHoveredProcessedArea: Function
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type coordinates = { x: number, y: number }
|
type coordinates = { x: number, y: number }
|
||||||
|
|
||||||
const Area = (props: Props) => {
|
const Area = (props: Props) => {
|
||||||
const { getProcessedAreaById, setSelectedAreaId } = useProject()
|
const { scale } = useSelector((state: RootState) => state.stage)
|
||||||
|
const { selectedAreaId, setSelectedAreaId } = useProject()
|
||||||
const shapeRef = React.useRef<Konva.Rect>(null)
|
const shapeRef = React.useRef<Konva.Rect>(null)
|
||||||
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
|
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
|
||||||
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()
|
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()
|
||||||
|
|
||||||
const { area, scale, isActive, setHoveredOverAreaIds, setHoveredProcessedArea } = props
|
const { area, isActive } = props
|
||||||
const a = area
|
const a = area
|
||||||
const width = (a.endX - a.startX)
|
const width = (a.endX - a.startX)
|
||||||
const height = (a.endY - a.startY)
|
const height = (a.endY - a.startY)
|
||||||
|
|
||||||
const handleEnterOrLeave = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
const stage = e.currentTarget.getStage()!
|
|
||||||
const currentMousePosition = stage.pointerPos
|
|
||||||
const intersectingNodes = stage.getAllIntersections(currentMousePosition)
|
|
||||||
const drawnAreas = intersectingNodes.filter(n => n.attrs?.isArea)
|
|
||||||
const drawnAreasIds = drawnAreas.map(d => d.attrs?.id)
|
|
||||||
setHoveredOverAreaIds(drawnAreasIds)
|
|
||||||
|
|
||||||
const processedAreaRequests = drawnAreasIds.map(a => getProcessedAreaById(a || ''))
|
|
||||||
Promise.all(processedAreaRequests).then(responses => {
|
|
||||||
const validResponses = responses.filter(r => r?.id) as entities.ProcessedArea[]
|
|
||||||
setHoveredProcessedArea(validResponses || [])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleContextMenu = (e: KonvaEventObject<PointerEvent>) => {
|
const handleContextMenu = (e: KonvaEventObject<PointerEvent>) => {
|
||||||
e.evt.preventDefault()
|
e.evt.preventDefault()
|
||||||
const stage = e.currentTarget.getStage()
|
const stage = e.currentTarget.getStage()
|
||||||
@ -56,6 +41,11 @@ const Area = (props: Props) => {
|
|||||||
setIsAreaContextMenuOpen(true)
|
setIsAreaContextMenuOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAreaClick = (areaId: string) => {
|
||||||
|
if (areaId === selectedAreaId) setSelectedAreaId('')
|
||||||
|
else setSelectedAreaId(areaId)
|
||||||
|
}
|
||||||
|
|
||||||
return <Group>
|
return <Group>
|
||||||
<Rect
|
<Rect
|
||||||
ref={shapeRef}
|
ref={shapeRef}
|
||||||
@ -70,19 +60,18 @@ const Area = (props: Props) => {
|
|||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
strokeScaleEnabled={false}
|
strokeScaleEnabled={false}
|
||||||
shadowForStrokeEnabled={false}
|
shadowForStrokeEnabled={false}
|
||||||
onMouseEnter={handleEnterOrLeave}
|
onClick={() => handleAreaClick(a.id)}
|
||||||
onMouseLeave={handleEnterOrLeave}
|
|
||||||
onDblClick={() => setSelectedAreaId(a.id)}
|
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
isArea />
|
isArea
|
||||||
{!isAreaContextMenuOpen
|
/>
|
||||||
? <></>
|
{isAreaContextMenuOpen
|
||||||
: <AreaContextMenu
|
? <AreaContextMenu
|
||||||
area={area}
|
area={area}
|
||||||
x={areaContextMenuPosition?.x || 0}
|
x={areaContextMenuPosition?.x || 0}
|
||||||
y={areaContextMenuPosition?.y || 0}
|
y={areaContextMenuPosition?.y || 0}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
setIsAreaContextMenuOpen={setIsAreaContextMenuOpen} />
|
setIsAreaContextMenuOpen={setIsAreaContextMenuOpen} />
|
||||||
|
: <></>
|
||||||
}
|
}
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { entities } from '../../../wailsjs/wailsjs/go/models'
|
import { entities } from '../../../wailsjs/wailsjs/go/models'
|
||||||
import { Html } from 'react-konva-utils'
|
import { Html } from 'react-konva-utils'
|
||||||
import { copyButtonColors, deleteButtonColors, makeFormStyles, makeSharedButtonStyles, reprocessButtonColors, setMutableStylesOnElement, setPosition, setScale } from './styles'
|
import { ClipboardIcon, ArrowPathIcon, TrashIcon, LanguageIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { getScaled, makeFormStyles, makeIconStyles } from './styles'
|
||||||
import { useProject } from '../../../context/Project/provider'
|
import { useProject } from '../../../context/Project/provider'
|
||||||
import asyncClick from '../../../utils/asyncClick'
|
import asyncClick from '../../../utils/asyncClick'
|
||||||
import processImageArea from '../../../useCases/processImageArea'
|
import processImageArea from '../../../useCases/processImageArea'
|
||||||
|
import classNames from '../../../utils/classNames'
|
||||||
|
import LanguageSelect from '../../utils/LanguageSelect'
|
||||||
|
import { RequestTranslateArea } from '../../../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
x: number,
|
x: number,
|
||||||
@ -16,75 +22,172 @@ type Props = {
|
|||||||
setIsAreaContextMenuOpen: Function
|
setIsAreaContextMenuOpen: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This uses Knova's HTML portal which does not support CSS classes.
|
|
||||||
* Because of this limitation we have to hack some UX with inline styles.
|
|
||||||
* @param {Props} props
|
|
||||||
*/
|
|
||||||
const AreaContextMenu = (props: Props) => {
|
|
||||||
const { getProcessedAreaById, requestDeleteAreaById, getSelectedDocument } = useProject()
|
|
||||||
const { area, setIsAreaContextMenuOpen, scale, x, y } = props
|
|
||||||
setPosition(x, y)
|
|
||||||
setScale(scale)
|
|
||||||
const sharedButtonStyles = makeSharedButtonStyles()
|
|
||||||
|
|
||||||
const handleBlur = (e: React.FocusEvent) => {
|
const AreaContextMenu = (props: Props) => {
|
||||||
console.log(e.relatedTarget)
|
const dispatch = useDispatch()
|
||||||
if (!e.currentTarget.contains(e.relatedTarget)) setIsAreaContextMenuOpen(false)
|
const { getProcessedAreaById, requestDeleteAreaById, getSelectedDocument, requestUpdateArea } = useProject()
|
||||||
}
|
const [shouldShowProcessLanguageSelect, setShouldShowProcessLanguageSelect] = useState(false)
|
||||||
|
const { area, setIsAreaContextMenuOpen, scale, x, y } = props
|
||||||
|
|
||||||
const handleCopyButtonClick = async () => {
|
const handleCopyButtonClick = async () => {
|
||||||
|
setIsAreaContextMenuOpen(false)
|
||||||
|
|
||||||
const processedArea = await getProcessedAreaById(area.id)
|
const processedArea = await getProcessedAreaById(area.id)
|
||||||
const wordsOfProcessedArea = processedArea?.lines.flatMap(l => l.words.map(w => w.fullText))
|
const wordsOfProcessedArea = processedArea?.lines.flatMap(l => l.words.map(w => w.fullText))
|
||||||
const fullText = wordsOfProcessedArea?.join(' ')
|
const fullText = wordsOfProcessedArea?.join(' ')
|
||||||
if (!fullText) return // TODO: change to show notification when copy fails
|
if (!fullText) {
|
||||||
|
dispatch(pushNotification({ message: 'No text found to copy.', level: 'warning' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await navigator.clipboard.writeText(fullText)
|
await navigator.clipboard.writeText(fullText)
|
||||||
setIsAreaContextMenuOpen(false)
|
dispatch(pushNotification({ message: 'Copied area to clipboard' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error copying area', level: 'error' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteButtonClick = async () => {
|
const handleDeleteButtonClick = async () => {
|
||||||
const response = await requestDeleteAreaById(area.id)
|
|
||||||
if (!response) return // TODO: change to show notification when copy fails
|
|
||||||
|
|
||||||
setIsAreaContextMenuOpen(false)
|
setIsAreaContextMenuOpen(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await requestDeleteAreaById(area.id)
|
||||||
|
if (!response) dispatch(pushNotification({ message: 'Could not delete area', level: 'warning' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error deleting area', level: 'error' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReprocessButtonClick = async () => {
|
const handleReprocessButtonClick = async () => {
|
||||||
const documentId = getSelectedDocument()?.id
|
setIsAreaContextMenuOpen(false)
|
||||||
if (!documentId) return // TODO: change to show notification when copy fails
|
|
||||||
|
|
||||||
setIsAreaContextMenuOpen(false) // TODO: possibly have loading animation and wait until after process
|
const documentId = getSelectedDocument()?.id
|
||||||
await processImageArea(documentId, area.id)
|
if (!documentId) {
|
||||||
|
dispatch(pushNotification({ message: 'Issue finding selected document', level: 'warning' }))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(pushNotification({ message: 'Processing test of area' }))
|
||||||
|
const response = await processImageArea(documentId, area.id)
|
||||||
|
if (response) dispatch(pushNotification({ message: 'Area successfully processed' }))
|
||||||
|
else dispatch(pushNotification({ message: 'No text result from processing area', level: 'warning' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleTranslateArea = async () => {
|
||||||
|
setIsAreaContextMenuOpen(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wasSuccessful = await RequestTranslateArea(area.id)
|
||||||
|
if (wasSuccessful) dispatch(pushNotification({ message: 'Successfully translated area' }))
|
||||||
|
else dispatch(pushNotification({ message: 'Issue translating area', level: 'warning' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error translating area', level: 'error' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
|
||||||
|
setIsAreaContextMenuOpen(false)
|
||||||
|
|
||||||
|
let successfullyUpdatedLanguageOnArea = false
|
||||||
|
try {
|
||||||
|
successfullyUpdatedLanguageOnArea = await requestUpdateArea({...area, ...{language: selectedLanguage}})
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error updating area language', level: 'error' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDocumentId = getSelectedDocument()?.id
|
||||||
|
if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) {
|
||||||
|
dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await processImageArea(selectedDocumentId, area.id)
|
||||||
|
dispatch(pushNotification({ message: 'Finished processing area', level: 'info' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnBlur = (e: React.FocusEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.relatedTarget === null) setIsAreaContextMenuOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseMenuItemClassNames = 'flex items-center justify-between w-full px-3 py-1 flex-shrink-0 text-left cursor-pointer focus:outline-none'
|
||||||
|
|
||||||
return <Html>
|
return <Html>
|
||||||
<form style={makeFormStyles()} onBlur={handleBlur}>
|
<div style={makeFormStyles(x, y, scale)} tabIndex={1} onBlur={handleOnBlur}>
|
||||||
<a
|
<div className={classNames(
|
||||||
tabIndex={-1}
|
'z-40 min-w-max py-1 rounded-lg shadow-sm outline-none font-light',
|
||||||
style={{ ...sharedButtonStyles, ...reprocessButtonColors.normal}}
|
'bg-white border border-gray-200',)}
|
||||||
onClick={(e) => asyncClick(e, handleCopyButtonClick)}
|
>
|
||||||
onMouseEnter={(e) => {setMutableStylesOnElement(e, copyButtonColors.hover)} }
|
|
||||||
onMouseLeave={(e) => {setMutableStylesOnElement(e, copyButtonColors.normal)} }>
|
<button autoFocus tabIndex={2}
|
||||||
Copy Area
|
onClick={(e) => asyncClick(e, handleCopyButtonClick)} className={
|
||||||
</a>
|
classNames(baseMenuItemClassNames,
|
||||||
<a
|
'focus:bg-neutral-100 hover:bg-slate-300',
|
||||||
tabIndex={-1}
|
)}>
|
||||||
style={{ ...sharedButtonStyles, ...reprocessButtonColors.normal}}
|
<span className="mr-2">Copy Area</span>
|
||||||
onClick={(e) => asyncClick(e, handleReprocessButtonClick)}
|
<ClipboardIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
|
||||||
onMouseEnter={(e) => {setMutableStylesOnElement(e, reprocessButtonColors.hover)} }
|
</button>
|
||||||
onMouseLeave={(e) => {setMutableStylesOnElement(e, reprocessButtonColors.normal)} }>
|
|
||||||
Reprocess
|
<button tabIndex={3}
|
||||||
</a>
|
onClick={(e) => asyncClick(e, handleReprocessButtonClick)} className={
|
||||||
<a
|
classNames(baseMenuItemClassNames,
|
||||||
tabIndex={-1}
|
'focus:bg-neutral-100 hover:bg-slate-300',
|
||||||
style={{ ...sharedButtonStyles, ...deleteButtonColors.normal}}
|
)}>
|
||||||
onClick={(e) => asyncClick(e, handleDeleteButtonClick)}
|
<span className="mr-2">Reprocess Area</span>
|
||||||
onMouseEnter={(e) => {setMutableStylesOnElement(e, deleteButtonColors.hover)} }
|
<ArrowPathIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
|
||||||
onMouseLeave={(e) => {setMutableStylesOnElement(e, deleteButtonColors.normal)} }>
|
</button>
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
</form>
|
|
||||||
|
<button tabIndex={3}
|
||||||
|
onClick={(e) => asyncClick(e, handleTranslateArea)} className={
|
||||||
|
classNames(baseMenuItemClassNames,
|
||||||
|
'focus:bg-neutral-100 hover:bg-slate-300',
|
||||||
|
)}>
|
||||||
|
<span className="mr-2">Translate Area</span>
|
||||||
|
<LanguageIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button tabIndex={4}
|
||||||
|
onClick={(e) => asyncClick(e, handleDeleteButtonClick)} className={
|
||||||
|
classNames(baseMenuItemClassNames,
|
||||||
|
'focus:bg-neutral-100 bg-red-100 text-gray-900 hover:text-gray-100 hover:bg-red-600',
|
||||||
|
)}>
|
||||||
|
<span className="mr-2">Delete Area</span>
|
||||||
|
<TrashIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{shouldShowProcessLanguageSelect
|
||||||
|
? <LanguageSelect
|
||||||
|
defaultLanguage={area.language || getSelectedDocument()?.defaultLanguage}
|
||||||
|
styles={{ fontSize: `${getScaled(14, scale)}px` }}
|
||||||
|
onSelect={handleProcessLanguageSelect}
|
||||||
|
/>
|
||||||
|
: <button tabIndex={5}
|
||||||
|
onClick={(e) => setShouldShowProcessLanguageSelect(true)}
|
||||||
|
className={classNames(
|
||||||
|
baseMenuItemClassNames,
|
||||||
|
'focus:bg-neutral-100 hover:bg-slate-300',
|
||||||
|
)}>
|
||||||
|
<span className="mr-2">
|
||||||
|
{area.language?.displayName || getSelectedDocument()?.defaultLanguage.displayName}
|
||||||
|
</span>
|
||||||
|
<LanguageIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Html >
|
</Html >
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,90 +1,31 @@
|
|||||||
import { DetailedHTMLProps, FormHTMLAttributes } from 'react'
|
import { DetailedHTMLProps, FormHTMLAttributes } from 'react'
|
||||||
|
|
||||||
let scale = 1
|
const getScaled = (value: number, scale: number) => Math.floor(value / scale)
|
||||||
const setScale = (newScale: number) => scale = newScale
|
|
||||||
const getScaled = (value: number) => Math.floor(value / scale)
|
|
||||||
|
|
||||||
let left = 0
|
|
||||||
let top = 0
|
|
||||||
const setPosition = (x: number, y: number) => {
|
|
||||||
left = x
|
|
||||||
top = y
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeProportionalStyles = () => ({
|
const makeFormStyles = (x: number, y: number, scale: number) => {
|
||||||
fontSize: getScaled(18),
|
const shadowOffset = { x: getScaled(4, scale), y: getScaled(4, scale), color: 'rgba(50, 50, 50, 0.4)', blur: getScaled(20, scale) }
|
||||||
radius: getScaled(6),
|
|
||||||
formPadding: getScaled(12),
|
|
||||||
buttonPadding: getScaled(4),
|
|
||||||
verticalMargin: getScaled(4),
|
|
||||||
shadowOffset: {
|
|
||||||
x: getScaled(4),
|
|
||||||
y: getScaled(4),
|
|
||||||
color: 'rgba(50, 50, 50, 0.4)',
|
|
||||||
blur: getScaled(20),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeFormStyles = () => {
|
|
||||||
const proportionalStyles = makeProportionalStyles()
|
|
||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: `${left}px`,
|
fontSize: `${getScaled(16, scale)}px`,
|
||||||
top: `${top}px`,
|
width: `${getScaled(224, scale)}px`,
|
||||||
textAlign: 'center',
|
left: `${x}px`,
|
||||||
display: 'block',
|
top: `${y}px`,
|
||||||
fontSize: `${proportionalStyles.fontSize}px`,
|
boxShadow: `${shadowOffset.x}px ${shadowOffset.y}px ${shadowOffset.blur}px ${shadowOffset.color}`
|
||||||
backgroundColor: 'rgb(229, 231, 235)',
|
|
||||||
borderRadius: `${proportionalStyles.radius}px`,
|
|
||||||
borderTopLeftRadius: '0px',
|
|
||||||
padding: `${proportionalStyles.formPadding}px`,
|
|
||||||
boxShadow: `${proportionalStyles.shadowOffset.x}px ${proportionalStyles.shadowOffset.y}px ${proportionalStyles.shadowOffset.blur}px ${proportionalStyles.shadowOffset.color}`
|
|
||||||
} as DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
|
} as DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeSharedButtonStyles = () => {
|
const makeIconStyles = (scale: number) => {
|
||||||
const proportionalStyles = makeProportionalStyles()
|
|
||||||
return {
|
return {
|
||||||
display: 'block',
|
width: `${getScaled(14, scale)}px`,
|
||||||
margin: `${proportionalStyles.verticalMargin}px auto`,
|
height: `${getScaled(14, scale)}px`
|
||||||
width: '100%',
|
|
||||||
border: 'solid 1px',
|
|
||||||
borderColor: 'rgb(31, 41, 55)',
|
|
||||||
borderRadius: `${proportionalStyles.radius}px`,
|
|
||||||
padding: `${proportionalStyles.buttonPadding}px`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reprocessButtonColors = {
|
|
||||||
normal: { color: '#414C61', backgroundColor: '#E5E7EB' },
|
|
||||||
hover: { color: '#E5E7EB', backgroundColor: '#9AB3E6' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyButtonColors = {
|
|
||||||
normal: { color: '#414C61', backgroundColor: '#E5E7EB' },
|
|
||||||
hover: { color: '#E5E7EB', backgroundColor: '#9AB3E6' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteButtonColors = {
|
|
||||||
normal: { color: '#DADCE0', backgroundColor: '#f87171' },
|
|
||||||
hover: { color: '#E5E7EB', backgroundColor: '#AD5050' },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Awful TS hackery
|
|
||||||
type styleDeclaration = Partial<CSSStyleDeclaration> & { [propName: string]: string };
|
|
||||||
const setMutableStylesOnElement = (e: React.MouseEvent<HTMLElement, MouseEvent>, stylesToSet: styleDeclaration) => {
|
|
||||||
for (const style in stylesToSet) {
|
|
||||||
(e.currentTarget.style as styleDeclaration)[style] = stylesToSet[style]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
setScale,
|
|
||||||
setPosition,
|
|
||||||
makeFormStyles,
|
makeFormStyles,
|
||||||
makeSharedButtonStyles,
|
makeIconStyles,
|
||||||
copyButtonColors,
|
getScaled,
|
||||||
deleteButtonColors,
|
|
||||||
reprocessButtonColors,
|
|
||||||
setMutableStylesOnElement,
|
|
||||||
}
|
}
|
||||||
@ -1,21 +1,36 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import { Group } from 'react-konva'
|
import { Group } from 'react-konva'
|
||||||
import { useProject } from '../../context/Project/provider'
|
import { useProject } from '../../context/Project/provider'
|
||||||
import { entities } from '../../wailsjs/wailsjs/go/models'
|
import { entities } from '../../wailsjs/wailsjs/go/models'
|
||||||
import Area from './Area'
|
import Area from './Area'
|
||||||
import ProcessedWord from './ProcessedWord'
|
import ProcessedWord from './ProcessedWord'
|
||||||
import EditingWord from './EditingWord'
|
import EditingWord from './EditingWord'
|
||||||
|
import { RootState } from '../../redux/store'
|
||||||
|
|
||||||
type Props = { scale: number }
|
type Props = { scale: number }
|
||||||
|
|
||||||
const Areas = ({ scale }: Props) => {
|
const Areas = ({ scale }: Props) => {
|
||||||
const { getSelectedDocument, selectedAreaId } = useProject()
|
const { areProcessedWordsVisible } = useSelector((state: RootState) => state.stage)
|
||||||
|
const { getSelectedDocument, selectedAreaId, getProcessedAreaById } = useProject()
|
||||||
const areas = getSelectedDocument()?.areas || []
|
const areas = getSelectedDocument()?.areas || []
|
||||||
const [hoveredOverAreaIds, setHoveredOverAreaIds] = useState<string[]>([])
|
|
||||||
const [hoveredProcessedAreas, setHoveredProcessedArea] = useState<entities.ProcessedArea[]>([])
|
|
||||||
const [editingWord, setEditingWord] = useState<entities.ProcessedWord | null>(null)
|
const [editingWord, setEditingWord] = useState<entities.ProcessedWord | null>(null)
|
||||||
|
const [selectedProcessedArea, setSelectedProcessedArea] = useState<entities.ProcessedArea | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedAreaId) return setSelectedProcessedArea(null)
|
||||||
|
else {
|
||||||
|
getProcessedAreaById(selectedAreaId).then(res => {
|
||||||
|
if (res) setSelectedProcessedArea(res)
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('getProcessedAreaById', err)
|
||||||
|
setSelectedProcessedArea(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [selectedAreaId])
|
||||||
|
|
||||||
const renderEditingWord = () => {
|
const renderEditingWord = () => {
|
||||||
if (!editingWord) return
|
if (!editingWord) return
|
||||||
@ -27,33 +42,26 @@ const Areas = ({ scale }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderProcessedWords = () => {
|
const renderProcessedWords = () => {
|
||||||
if (!hoveredProcessedAreas.length) return
|
if (!selectedProcessedArea) return <></>
|
||||||
|
|
||||||
return hoveredProcessedAreas.map(a => {
|
const words = selectedProcessedArea.lines.map(l => l.words).flat()
|
||||||
const words = a.lines.map(l => l.words).flat()
|
|
||||||
return words.map((w, index) => <ProcessedWord
|
return words.map((w, index) => <ProcessedWord
|
||||||
key={index}
|
key={index}
|
||||||
area={a}
|
area={selectedProcessedArea}
|
||||||
word={w}
|
word={w}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
setEditingWord={setEditingWord}
|
setEditingWord={setEditingWord}
|
||||||
/>)
|
/>)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => {
|
const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => {
|
||||||
return <Area key={index}
|
return <Area key={index} area={a} isActive={a.id === selectedAreaId} />
|
||||||
area={a}
|
|
||||||
scale={scale}
|
|
||||||
setHoveredOverAreaIds={setHoveredOverAreaIds}
|
|
||||||
setHoveredProcessedArea={setHoveredProcessedArea}
|
|
||||||
isActive={(hoveredOverAreaIds.includes(a.id) || a.id === selectedAreaId)} />
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Group>
|
return <Group>
|
||||||
{renderAreas(areas)}
|
{renderAreas(areas)}
|
||||||
{renderProcessedWords()}
|
{areProcessedWordsVisible ? renderProcessedWords() : <></>}
|
||||||
{renderEditingWord()}
|
{areProcessedWordsVisible ? renderEditingWord() : <></>}
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useRef, useState } from 'react'
|
import React, { useRef, useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { Stage, Layer, Image, } from 'react-konva'
|
import { Stage, Layer, Image, } from 'react-konva'
|
||||||
import { KonvaEventObject } from 'konva/lib/Node'
|
import { KonvaEventObject } from 'konva/lib/Node'
|
||||||
import Areas from './Areas'
|
import Areas from './Areas'
|
||||||
@ -9,22 +10,25 @@ import useImage from 'use-image'
|
|||||||
import { RectangleCoordinates } from './types'
|
import { RectangleCoordinates } from './types'
|
||||||
import DrawingArea from './DrawingArea'
|
import DrawingArea from './DrawingArea'
|
||||||
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
|
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
|
||||||
import processImageArea from '../../useCases/processImageArea'
|
import ContextConnections from './ContextConnections'
|
||||||
|
import processImageRect from '../../useCases/processImageRect'
|
||||||
type Props = {
|
import { RootState } from '../../redux/store'
|
||||||
scale: number,
|
import { maxScale, scaleStep, setIsDrawingArea, setScale, setStartingContextConnectionPoint } from '../../redux/features/stage/stageSlice'
|
||||||
scaleStep: number,
|
|
||||||
maxScale: number,
|
|
||||||
setScale: Function,
|
|
||||||
size: { width: number, height: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
let downClickX: number
|
let downClickX: number
|
||||||
let downClickY: number
|
let downClickY: number
|
||||||
let isDrawing = false
|
|
||||||
|
|
||||||
const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
|
const CanvasStage = () => {
|
||||||
const { getSelectedDocument, requestAddArea, setSelectedAreaId } = useProject()
|
const dispatch = useDispatch()
|
||||||
|
const {
|
||||||
|
scale, size,
|
||||||
|
isDrawingArea,
|
||||||
|
areAreasVisible,
|
||||||
|
areLinkAreaContextsVisible,
|
||||||
|
startingContextConnectionPoint
|
||||||
|
} = useSelector((state: RootState) => state.stage)
|
||||||
|
|
||||||
|
const { getSelectedDocument, updateDocuments, setSelectedAreaId } = useProject()
|
||||||
const [documentImage] = useImage(getSelectedDocument()?.path || '')
|
const [documentImage] = useImage(getSelectedDocument()?.path || '')
|
||||||
const documentRef = useRef(null)
|
const documentRef = useRef(null)
|
||||||
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
|
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
|
||||||
@ -33,17 +37,18 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
|
|||||||
const documentHeight = documentImage?.naturalHeight || 0
|
const documentHeight = documentImage?.naturalHeight || 0
|
||||||
|
|
||||||
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (startingContextConnectionPoint) return dispatch(setStartingContextConnectionPoint(null)) // TODO: handle if clicking o connect
|
||||||
if (!e.evt.shiftKey) return e.currentTarget.startDrag()
|
if (!e.evt.shiftKey) return e.currentTarget.startDrag()
|
||||||
|
|
||||||
const position = e.currentTarget.getRelativePointerPosition()
|
const position = e.currentTarget.getRelativePointerPosition()
|
||||||
downClickX = position.x
|
downClickX = position.x
|
||||||
downClickY = position.y
|
downClickY = position.y
|
||||||
isDrawing = true
|
dispatch(setIsDrawingArea(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
const currentPosition = e.currentTarget.getRelativePointerPosition()
|
const currentPosition = e.currentTarget.getRelativePointerPosition()
|
||||||
if (isDrawing) return setDrawingAreaRect({
|
if (isDrawingArea) return setDrawingAreaRect({
|
||||||
startX: downClickX,
|
startX: downClickX,
|
||||||
startY: downClickY,
|
startY: downClickY,
|
||||||
endX: currentPosition.x,
|
endX: currentPosition.x,
|
||||||
@ -54,17 +59,18 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
|
|||||||
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
|
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
const stage = e.currentTarget
|
const stage = e.currentTarget
|
||||||
if (stage.isDragging()) stage.stopDrag()
|
if (stage.isDragging()) stage.stopDrag()
|
||||||
if (isDrawing) isDrawing = false
|
else if (isDrawingArea) dispatch(setIsDrawingArea(false))
|
||||||
|
|
||||||
if (!drawingAreaRect) return
|
if (!drawingAreaRect) return
|
||||||
|
|
||||||
const normalizedDrawnRect = getNormalizedRectToBounds(drawingAreaRect, documentWidth, documentHeight, scale)
|
const normalizedDrawnRect = getNormalizedRectToBounds(drawingAreaRect, documentWidth, documentHeight, scale)
|
||||||
const selectedDocumentId = getSelectedDocument()!.id
|
const selectedDocumentId = getSelectedDocument()!.id
|
||||||
requestAddArea(selectedDocumentId, normalizedDrawnRect).then(addedArea => {
|
processImageRect(selectedDocumentId, normalizedDrawnRect).then(async addedAreas => {
|
||||||
setSelectedAreaId(addedArea.id)
|
updateDocuments().then(response => {
|
||||||
processImageArea(selectedDocumentId, addedArea.id)
|
if (!addedAreas.length) return
|
||||||
|
setSelectedAreaId(addedAreas[0].id)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
setDrawingAreaRect(null)
|
setDrawingAreaRect(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +80,8 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
|
|||||||
const wheelDelta = e.evt.deltaY
|
const wheelDelta = e.evt.deltaY
|
||||||
|
|
||||||
const shouldAttemptScaleUp = (wheelDelta < 0) && scale < maxScale
|
const shouldAttemptScaleUp = (wheelDelta < 0) && scale < maxScale
|
||||||
if (shouldAttemptScaleUp) setScale(scale + scaleStep)
|
if (shouldAttemptScaleUp) dispatch(setScale(scale + scaleStep))
|
||||||
else if (scale > (scaleStep * 2)) setScale(scale - scaleStep)
|
else if (scale > (scaleStep * 2)) dispatch(setScale(scale - scaleStep))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Stage width={size.width} height={size.height} scale={{ x: scale, y: scale }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onWheel={handleWheel}>
|
return <Stage width={size.width} height={size.height} scale={{ x: scale, y: scale }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onWheel={handleWheel}>
|
||||||
@ -92,11 +98,20 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
|
|||||||
shadowBlur={documentWidth * 0.05}
|
shadowBlur={documentWidth * 0.05}
|
||||||
listening={false}
|
listening={false}
|
||||||
/>
|
/>
|
||||||
{(isDrawing && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
|
{(isDrawingArea && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
|
||||||
</Layer>
|
</Layer>
|
||||||
<Layer>
|
{areAreasVisible
|
||||||
|
? <Layer id='areaLayer'>
|
||||||
<Areas scale={scale} />
|
<Areas scale={scale} />
|
||||||
</Layer>
|
</Layer>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
{areAreasVisible && areLinkAreaContextsVisible
|
||||||
|
? <Layer id='contextConnections'>
|
||||||
|
<ContextConnections />
|
||||||
|
</Layer>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
</Stage>
|
</Stage>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { Group, Line } from 'react-konva'
|
||||||
|
import { useProject } from '../../../context/Project/provider'
|
||||||
|
import { RootState } from '../../../redux/store'
|
||||||
|
|
||||||
|
const ConnectionLines = () => {
|
||||||
|
const { scale } = useSelector((state: RootState) => state.stage)
|
||||||
|
const { getSelectedDocument, contextGroups } = useProject()
|
||||||
|
const areas = getSelectedDocument()?.areas || []
|
||||||
|
|
||||||
|
const renderLines = () => {
|
||||||
|
if (!contextGroups?.length) return <></>
|
||||||
|
|
||||||
|
const linesAlreadyRendered = new Set<string>()
|
||||||
|
const lines = contextGroups.map((contextGroup) => {
|
||||||
|
const currentArea = areas.find(a => a.id === contextGroup.areaId)
|
||||||
|
const nextArea = areas.find(a => a.id === contextGroup.nextId)
|
||||||
|
if (!currentArea || !nextArea) return
|
||||||
|
|
||||||
|
if (linesAlreadyRendered.has(`${contextGroup.areaId}-${contextGroup.nextId}`)) return
|
||||||
|
if (linesAlreadyRendered.has(`${contextGroup.nextId}-${contextGroup.areaId}`)) return
|
||||||
|
|
||||||
|
const startingPoint = {
|
||||||
|
x: ((currentArea.startX + currentArea.endX) * scale) / 2,
|
||||||
|
y: currentArea.startY * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingTensionPoint = {
|
||||||
|
x: (startingPoint.x + (nextArea.startX * scale)) / 2,
|
||||||
|
y: startingPoint.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
const endingPoint = {
|
||||||
|
x: ((nextArea.startX + nextArea.endX) * scale) / 2,
|
||||||
|
y: nextArea.endY * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const endingTensionPoint = {
|
||||||
|
x: (startingPoint.x + (nextArea.startX * scale)) / 2,
|
||||||
|
y: endingPoint.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
linesAlreadyRendered.add(`${contextGroup.areaId}-${contextGroup.nextId}`)
|
||||||
|
linesAlreadyRendered.add(`${contextGroup.nextId}-${contextGroup.areaId}`)
|
||||||
|
|
||||||
|
return <Line
|
||||||
|
key={`${contextGroup.areaId}-${contextGroup.nextId}`}
|
||||||
|
points={[
|
||||||
|
...Object.values(startingPoint),
|
||||||
|
...Object.values(startingTensionPoint),
|
||||||
|
...Object.values(endingTensionPoint),
|
||||||
|
...Object.values(endingPoint),
|
||||||
|
]}
|
||||||
|
strokeEnabled
|
||||||
|
strokeWidth={2 * scale}
|
||||||
|
stroke='#dc8dec'
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
tension={0.2}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
return lines.filter(l => !!l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Group>{renderLines()}</Group>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConnectionLines
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Circle, Group } from 'react-konva'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { entities } from '../../../wailsjs/wailsjs/go/models'
|
||||||
|
import { KonvaEventObject } from 'konva/lib/Node'
|
||||||
|
import { useProject } from '../../../context/Project/provider'
|
||||||
|
import { RootState } from '../../../redux/store'
|
||||||
|
import { setStartingContextConnectionPoint } from '../../../redux/features/stage/stageSlice'
|
||||||
|
|
||||||
|
type Props = { areas: entities.Area[] }
|
||||||
|
const ConnectionPoints = (props: Props) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const { scale, areLinkAreaContextsVisible, startingContextConnectionPoint } = useSelector((state: RootState) => state.stage)
|
||||||
|
|
||||||
|
const { requestConnectProcessedAreas } = useProject()
|
||||||
|
|
||||||
|
const handleContextAreaMouseDown = async (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
e.cancelBubble = true
|
||||||
|
const clickedConnectionPoint = {
|
||||||
|
isHead: e.currentTarget.attrs.isHead,
|
||||||
|
areaId: e.currentTarget.attrs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startingContextConnectionPoint) return dispatch(setStartingContextConnectionPoint(clickedConnectionPoint))
|
||||||
|
|
||||||
|
if (clickedConnectionPoint.isHead === startingContextConnectionPoint.isHead
|
||||||
|
|| clickedConnectionPoint.areaId === startingContextConnectionPoint.areaId)
|
||||||
|
return dispatch(setStartingContextConnectionPoint(null))
|
||||||
|
|
||||||
|
const headId = startingContextConnectionPoint.isHead ? startingContextConnectionPoint.areaId : clickedConnectionPoint.areaId
|
||||||
|
const tailId = !startingContextConnectionPoint.isHead ? startingContextConnectionPoint.areaId : clickedConnectionPoint.areaId
|
||||||
|
dispatch(setStartingContextConnectionPoint(null))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestConnectProcessedAreas(headId, tailId)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('RequestConnectProcessedAreas', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderConnectingPointsForArea = (a: entities.Area) => {
|
||||||
|
if (!areLinkAreaContextsVisible) return <></>
|
||||||
|
|
||||||
|
const headConnector = <Circle
|
||||||
|
key={`head-${a.id}`}
|
||||||
|
id={a.id}
|
||||||
|
radius={8}
|
||||||
|
x={((a.startX + a.endX) * scale) / 2}
|
||||||
|
y={a.startY * scale}
|
||||||
|
strokeEnabled={false}
|
||||||
|
fill='#dc8dec'
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
onMouseDown={handleContextAreaMouseDown}
|
||||||
|
isHead
|
||||||
|
/>
|
||||||
|
|
||||||
|
const tailConnector = <Circle
|
||||||
|
key={`tail-${a.id}`}
|
||||||
|
id={a.id}
|
||||||
|
radius={10}
|
||||||
|
x={((a.startX + a.endX) * scale) / 2}
|
||||||
|
y={a.endY * scale}
|
||||||
|
strokeEnabled={false}
|
||||||
|
fill='#1e1e1e'
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
onMouseDown={handleContextAreaMouseDown}
|
||||||
|
isHead={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
let connectorsToRender = []
|
||||||
|
|
||||||
|
if (!startingContextConnectionPoint) connectorsToRender = [headConnector, tailConnector]
|
||||||
|
else if (startingContextConnectionPoint.isHead) connectorsToRender = [tailConnector]
|
||||||
|
else connectorsToRender = [headConnector]
|
||||||
|
|
||||||
|
if (startingContextConnectionPoint?.areaId === a.id) {
|
||||||
|
let y = (startingContextConnectionPoint.isHead ? a.startY : a.endY) * scale
|
||||||
|
connectorsToRender.push(<Circle
|
||||||
|
key={`active-${a.id}`}
|
||||||
|
id={a.id}
|
||||||
|
radius={10}
|
||||||
|
x={((a.startX + a.endX) * scale) / 2}
|
||||||
|
y={y}
|
||||||
|
strokeEnabled={false}
|
||||||
|
fill={startingContextConnectionPoint.isHead ? '#dc8dec' : '#1e1e1e'}
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
isHead={startingContextConnectionPoint.isHead}
|
||||||
|
onMouseDown={() => dispatch(setStartingContextConnectionPoint(null))}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Group key={`group-${a.id}`}>
|
||||||
|
{connectorsToRender}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAllConnectingPoints = () => props.areas.map(a => renderConnectingPointsForArea(a))
|
||||||
|
|
||||||
|
return <Group>
|
||||||
|
{renderAllConnectingPoints()}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConnectionPoints
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { Line } from 'react-konva'
|
||||||
|
import { Coordinates } from '../types'
|
||||||
|
import { useProject } from '../../../context/Project/provider'
|
||||||
|
import { RootState } from '../../../redux/store'
|
||||||
|
|
||||||
|
type CurrentDrawingConnectionProps = {
|
||||||
|
endDrawingPosition: Coordinates | null
|
||||||
|
}
|
||||||
|
const CurrentDrawingConnection = (props: CurrentDrawingConnectionProps) => {
|
||||||
|
const { scale, startingContextConnectionPoint } = useSelector((state: RootState) => state.stage)
|
||||||
|
const { endDrawingPosition } = props
|
||||||
|
const { getSelectedDocument } = useProject()
|
||||||
|
const areas = getSelectedDocument()?.areas || []
|
||||||
|
|
||||||
|
if (!startingContextConnectionPoint || !endDrawingPosition) return <></>
|
||||||
|
|
||||||
|
const { areaId, isHead } = startingContextConnectionPoint
|
||||||
|
|
||||||
|
const area = areas.find(a => a.id === areaId)
|
||||||
|
if (!area) return <></>
|
||||||
|
|
||||||
|
const startingPoint = {
|
||||||
|
x: ((area.startX + area.endX) * scale) / 2,
|
||||||
|
y: (isHead ? area.startY : area.endY) * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingTensionPoint = {
|
||||||
|
x: (startingPoint.x + endDrawingPosition.x) / 2,
|
||||||
|
y: startingPoint.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
const endingTensionPoint = {
|
||||||
|
x: (startingPoint.x + endDrawingPosition.x) / 2,
|
||||||
|
y: endDrawingPosition.y,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Line
|
||||||
|
points={[
|
||||||
|
...Object.values(startingPoint),
|
||||||
|
...Object.values(startingTensionPoint),
|
||||||
|
...Object.values(endingTensionPoint),
|
||||||
|
...Object.values(endDrawingPosition),
|
||||||
|
]}
|
||||||
|
strokeEnabled
|
||||||
|
strokeWidth={2 * scale}
|
||||||
|
stroke='#dc8dec'
|
||||||
|
strokeScaleEnabled={false}
|
||||||
|
shadowForStrokeEnabled={false}
|
||||||
|
tension={0.2}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CurrentDrawingConnection
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { Group } from 'react-konva'
|
||||||
|
import { useProject } from '../../../context/Project/provider'
|
||||||
|
import Konva from 'konva'
|
||||||
|
import { Coordinates } from '../types'
|
||||||
|
import CurrentDrawingConnection from './CurrentDrawingConnection'
|
||||||
|
import ConnectionPoints from './ConnectionPoints'
|
||||||
|
import ConnectionLines from './ConnectionLines'
|
||||||
|
import { RootState } from '../../../redux/store'
|
||||||
|
|
||||||
|
const ContextConnections = () => {
|
||||||
|
const { startingContextConnectionPoint, areLinkAreaContextsVisible } = useSelector((state: RootState) => state.stage)
|
||||||
|
|
||||||
|
const { getSelectedDocument } = useProject()
|
||||||
|
const areas = getSelectedDocument()?.areas || []
|
||||||
|
|
||||||
|
const [endDrawingPosition, setEndDrawingPosition] = useState<Coordinates | null>(null)
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!areLinkAreaContextsVisible || !startingContextConnectionPoint) return
|
||||||
|
setEndDrawingPosition(Konva.stages[0].getRelativePointerPosition())
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove)
|
||||||
|
return () => window.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!startingContextConnectionPoint) setEndDrawingPosition(null)
|
||||||
|
}, [startingContextConnectionPoint])
|
||||||
|
|
||||||
|
if (!areLinkAreaContextsVisible) return <></>
|
||||||
|
|
||||||
|
return <Group>
|
||||||
|
<ConnectionPoints areas={areas} />
|
||||||
|
<ConnectionLines />
|
||||||
|
<CurrentDrawingConnection endDrawingPosition={endDrawingPosition} />
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextConnections
|
||||||
@ -36,7 +36,7 @@ const EditingWord = (props: Props) => {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
fontSize: `${Math.floor(48 * scale)}px`,
|
fontSize: `${Math.floor(24 * scale)}px`,
|
||||||
alignContent: 'center',
|
alignContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const ProcessedWord = (props: Props) => {
|
|||||||
height={y1 - y0}
|
height={y1 - y0}
|
||||||
scale={{ x: scale, y: scale }}
|
scale={{ x: scale, y: scale }}
|
||||||
x={x0 * scale}
|
x={x0 * scale}
|
||||||
y={y1 * scale}
|
y={y0 * scale}
|
||||||
strokeEnabled={false}
|
strokeEnabled={false}
|
||||||
shadowForStrokeEnabled={false}
|
shadowForStrokeEnabled={false}
|
||||||
strokeScaleEnabled={false}
|
strokeScaleEnabled={false}
|
||||||
@ -42,7 +42,7 @@ const ProcessedWord = (props: Props) => {
|
|||||||
height={y1 - y0}
|
height={y1 - y0}
|
||||||
scale={{ x: scale, y: scale }}
|
scale={{ x: scale, y: scale }}
|
||||||
x={x0 * scale}
|
x={x0 * scale}
|
||||||
y={y1 * scale}
|
y={y0 * scale}
|
||||||
align='center'
|
align='center'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize={36}
|
fontSize={36}
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from '../../../utils/classNames'
|
||||||
|
|
||||||
|
type Icon = (props: React.SVGProps<SVGSVGElement> & {
|
||||||
|
title?: string | undefined;
|
||||||
|
titleId?: string | undefined;
|
||||||
|
}) => JSX.Element
|
||||||
|
|
||||||
|
const ToolToggleButton = (props: { icon: Icon, hint: string, isActive: boolean, onClick?: React.MouseEventHandler<HTMLButtonElement> }) => {
|
||||||
|
return <div className="group flex relative">
|
||||||
|
<button className='pointer-events-auto p-2 bg-white rounded-md block mt-3 shadow-lg hover:bg-slate-100 aria-pressed:bg-indigo-400 aria-pressed:text-white'
|
||||||
|
aria-pressed={props.isActive}
|
||||||
|
onClick={props.onClick}>
|
||||||
|
<props.icon className='w-5 h-5' />
|
||||||
|
</button>
|
||||||
|
<div className={classNames(
|
||||||
|
'group-hover:opacity-100 transition-opacity0 p-1',
|
||||||
|
'absolute -translate-x-full opacity-0 m-4 mx-auto',
|
||||||
|
)}>
|
||||||
|
<div className={'bg-gray-800 p-1 text-xs text-gray-100 rounded-md'}>
|
||||||
|
{props.hint}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToolToggleButton
|
||||||
122
frontend/components/DocumentCanvas/ToolingOverlay/index.tsx
Normal file
122
frontend/components/DocumentCanvas/ToolingOverlay/index.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { DocumentTextIcon, LanguageIcon, LinkIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, SquaresPlusIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useProject } from '../../../context/Project/provider'
|
||||||
|
import { entities } from '../../../wailsjs/wailsjs/go/models'
|
||||||
|
import LanguageSelect from '../../utils/LanguageSelect'
|
||||||
|
import ToolToggleButton from './ToolToggleButton'
|
||||||
|
import processImageArea from '../../../useCases/processImageArea'
|
||||||
|
import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice'
|
||||||
|
import { RootState } from '../../../redux/store'
|
||||||
|
import { maxScale, scaleStep, setAreAreasVisible, setAreLinkAreaContextsVisible, setAreProcessedWordsVisible, setAreTranslatedWordsVisible, setScale } from '../../../redux/features/stage/stageSlice'
|
||||||
|
|
||||||
|
|
||||||
|
const ToolingOverlay = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const {
|
||||||
|
scale,
|
||||||
|
areAreasVisible,
|
||||||
|
areLinkAreaContextsVisible,
|
||||||
|
areProcessedWordsVisible,
|
||||||
|
areTranslatedWordsVisible,
|
||||||
|
} = useSelector((state: RootState) => state.stage)
|
||||||
|
|
||||||
|
const { getSelectedDocument, selectedAreaId, requestUpdateArea, requestUpdateDocument, updateDocuments } = useProject()
|
||||||
|
|
||||||
|
const selectedDocument = getSelectedDocument()
|
||||||
|
const [selectedArea, setSelectedArea] = useState<entities.Area | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedArea(selectedDocument?.areas.find(a => a.id == selectedAreaId))
|
||||||
|
}, [selectedAreaId, selectedDocument, selectedArea])
|
||||||
|
|
||||||
|
const handleAreaProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
|
||||||
|
if (!selectedArea) return
|
||||||
|
|
||||||
|
let successfullyUpdatedLanguageOnArea = false
|
||||||
|
try {
|
||||||
|
successfullyUpdatedLanguageOnArea = await requestUpdateArea({ ...selectedArea, ...{ language: selectedLanguage } })
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error updating area language', level: 'error' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDocumentId = getSelectedDocument()?.id
|
||||||
|
if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) {
|
||||||
|
dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await processImageArea(selectedDocumentId, selectedArea.id)
|
||||||
|
await updateDocuments()
|
||||||
|
dispatch(pushNotification({ message: 'Finished processing area', level: 'info' }))
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(pushNotification({ message: 'Error processing area', level: 'error' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDocumentProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
|
||||||
|
if (!selectedDocument) return
|
||||||
|
|
||||||
|
const currentDocument = selectedDocument
|
||||||
|
currentDocument.defaultLanguage = selectedLanguage
|
||||||
|
await requestUpdateDocument(currentDocument)
|
||||||
|
await updateDocuments()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const renderLanguageSelect = () => {
|
||||||
|
const defaultLanguage = selectedArea?.language.displayName ? selectedArea?.language : selectedDocument?.defaultLanguage
|
||||||
|
const onSelect = selectedArea ? handleAreaProcessLanguageSelect : handleDocumentProcessLanguageSelect
|
||||||
|
|
||||||
|
return <LanguageSelect
|
||||||
|
styles={{ fontSize: '16px', borderRadius: '2px' }}
|
||||||
|
defaultLanguage={defaultLanguage}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{/* Top buttons */}
|
||||||
|
<div className='absolute flex justify-between align-top top-2 p-2 drop-shadow-2xl pointer-events-none shadow-slate-100' style={{ width: 'calc(100% - 0.5rem)' }}>
|
||||||
|
<div className='align-top pointer-events-auto w-1/3'>
|
||||||
|
<h1 className="text-lg font-medium text-gray-900 block mr-2 drop-shadow-2xl shadow-slate-100 drop truncate">
|
||||||
|
{selectedArea?.name
|
||||||
|
? `${selectedDocument?.name} / ${selectedArea?.name}`
|
||||||
|
: selectedDocument?.name
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
{ renderLanguageSelect() }
|
||||||
|
{/* <LanguageSelect styles={{ fontSize: '16px', borderRadius: '2px' }} defaultLanguage={selectedArea?.language.displayName ? selectedArea?.language : selectedDocument?.defaultLanguage} /> */}
|
||||||
|
</div>
|
||||||
|
<div className='flex mt-4 justify-evenly align-top pointer-events-auto'>
|
||||||
|
<MagnifyingGlassMinusIcon className='w-4 h-4' />
|
||||||
|
<input
|
||||||
|
id="zoomRange" type="range" min={scaleStep} max={maxScale} step={scaleStep}
|
||||||
|
value={scale} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
|
||||||
|
onChange={(e) => { dispatch(setScale(e.currentTarget.valueAsNumber)) }}
|
||||||
|
/>
|
||||||
|
<MagnifyingGlassPlusIcon className='w-4 h-4' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Buttons */}
|
||||||
|
<div className='absolute bottom-6 right-3 pointer-events-none'>
|
||||||
|
{areAreasVisible
|
||||||
|
? <>
|
||||||
|
<ToolToggleButton icon={LinkIcon} hint='Link Area Contexts' isActive={areLinkAreaContextsVisible} onClick={() => dispatch(setAreLinkAreaContextsVisible(!areLinkAreaContextsVisible))} />
|
||||||
|
<ToolToggleButton icon={LanguageIcon} hint='Toggle Translations' isActive={areTranslatedWordsVisible} onClick={() => dispatch(setAreTranslatedWordsVisible(!areTranslatedWordsVisible))} />
|
||||||
|
<ToolToggleButton icon={DocumentTextIcon} hint='Toggle Processed' isActive={areProcessedWordsVisible} onClick={() => dispatch(setAreProcessedWordsVisible(!areProcessedWordsVisible))} />
|
||||||
|
</>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ToolToggleButton icon={SquaresPlusIcon} hint='Toggle Areas' isActive={areAreasVisible} onClick={() => dispatch(setAreAreasVisible(!areAreasVisible))} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToolingOverlay
|
||||||
@ -1,31 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useProject, } from '../../context/Project/provider'
|
import { useDispatch } from 'react-redux'
|
||||||
import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline'
|
import ToolingOverlay from './ToolingOverlay'
|
||||||
import LanguageSelect from '../workspace/LanguageSelect'
|
import { setSize } from '../../redux/features/stage/stageSlice'
|
||||||
|
|
||||||
const CanvasStage = dynamic(() => import('./CanvasStage'), {
|
const CanvasStage = dynamic(() => import('./CanvasStage'), { ssr: false })
|
||||||
ssr: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const zoomStep = 0.01
|
|
||||||
const maxZoomLevel = 4
|
|
||||||
|
|
||||||
const DocumentCanvas = () => {
|
const DocumentCanvas = () => {
|
||||||
const { getSelectedDocument } = useProject()
|
const dispatch = useDispatch()
|
||||||
const selectedDocument = getSelectedDocument()
|
|
||||||
|
|
||||||
const [zoomLevel, setZoomLevel] = useState(1)
|
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 })
|
|
||||||
const thisRef = useRef<HTMLDivElement>(null)
|
const thisRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
|
||||||
const handleWindowResize = () => {
|
const handleWindowResize = () => {
|
||||||
const width = thisRef?.current?.clientWidth || 0
|
const width = thisRef?.current?.clientWidth || 0
|
||||||
const height = thisRef?.current?.clientHeight || 0
|
const height = thisRef?.current?.clientHeight || 0
|
||||||
setSize({ width, height })
|
dispatch(setSize({ width, height }))
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,25 +25,10 @@ const DocumentCanvas = () => {
|
|||||||
return () => window.removeEventListener('resize', handleWindowResize)
|
return () => window.removeEventListener('resize', handleWindowResize)
|
||||||
}, [thisRef?.current?.clientWidth, thisRef?.current?.clientHeight])
|
}, [thisRef?.current?.clientWidth, thisRef?.current?.clientHeight])
|
||||||
|
|
||||||
return <div ref={thisRef} className='relative' style={{ height: 'calc(100vh - 200px)' }}>
|
return <div ref={thisRef} className='relative' style={{ height: 'calc(100vh - 140px)' }}>
|
||||||
<div className='flex justify-between align-top mb-2'>
|
|
||||||
<div className='flex align-top'>
|
|
||||||
<h1 className="text-xl font-semibold text-gray-900 inline-block mr-2">{selectedDocument?.name}</h1>
|
|
||||||
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-evenly items-center'>
|
|
||||||
<MagnifyingGlassMinusIcon className='w-4 h-4' />
|
|
||||||
<input
|
|
||||||
id="zoomRange" type="range" min={zoomStep} max={maxZoomLevel} step={zoomStep}
|
|
||||||
value={zoomLevel} className="w-[calc(100%-50px)] h-2 bg-indigo-200 rounded-lg appearance-none cursor-pointer p-0"
|
|
||||||
onChange={(e) => { setZoomLevel(e.currentTarget.valueAsNumber) }}
|
|
||||||
/>
|
|
||||||
<MagnifyingGlassPlusIcon className='w-4 h-4' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='h-full overflow-hidden rounded-lg border-4 border-dashed border-gray-200'>
|
<div className='h-full overflow-hidden rounded-lg border-4 border-dashed border-gray-200'>
|
||||||
<CanvasStage size={size} scale={zoomLevel} scaleStep={zoomStep} setScale={setZoomLevel} maxScale={maxZoomLevel} />
|
<CanvasStage />
|
||||||
|
<ToolingOverlay />
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export type RectangleCoordinates = {
|
|||||||
startX: number, startY: number, endX: number, endY: number
|
startX: number, startY: number, endX: number, endY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Coordinates = { x: number, y: number }
|
||||||
|
|
||||||
export type AddAreaToStoreCallback = (startX: number, startY: number, endX: number, endY: number) => Promise<void>
|
export type AddAreaToStoreCallback = (startX: number, startY: number, endX: number, endY: number) => Promise<void>
|
||||||
|
|
||||||
export type SetZoomCallback = (newZoomLevel: number) => void
|
export type SetZoomCallback = (newZoomLevel: number) => void
|
||||||
|
|||||||
86
frontend/components/Notifications/index.tsx
Normal file
86
frontend/components/Notifications/index.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Fragment, useEffect } from 'react'
|
||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { XMarkIcon, InformationCircleIcon, ExclamationTriangleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { RootState } from '../../redux/store'
|
||||||
|
import { NotificationProps } from '../../redux/features/notifications/types'
|
||||||
|
import { dismissCurrentNotification } from '../../redux/features/notifications/notificationQueueSlice'
|
||||||
|
|
||||||
|
const renderIcon = (level: NotificationProps['level'] = 'info') => {
|
||||||
|
switch (level) {
|
||||||
|
default: return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
|
||||||
|
case 'info': return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
|
||||||
|
case 'warning': return <ExclamationTriangleIcon className='w-6 h-6 text-orange-400' />
|
||||||
|
case 'error': return <ExclamationCircleIcon className='w-6 h-6 text-red-600' />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationTime = 3000
|
||||||
|
|
||||||
|
const Notification = () => {
|
||||||
|
const { currentNotification, queue } = useSelector((state: RootState) => state.notificationQueue)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (currentNotification?.onActionClickCallback) currentNotification?.onActionClickCallback()
|
||||||
|
if (currentNotification?.closeOnAction) dispatch(dismissCurrentNotification())
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queue.length) setTimeout(() => dispatch(dismissCurrentNotification()), notificationTime)
|
||||||
|
}, [currentNotification])
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div
|
||||||
|
aria-live="assertive"
|
||||||
|
className="pointer-events-none absolute block top-0 left-0 w-full h-full"
|
||||||
|
>
|
||||||
|
<div className="absolute items-center" style={{ bottom: '12px', right: '16px' }}>
|
||||||
|
<Transition
|
||||||
|
show={!!currentNotification}
|
||||||
|
as={Fragment}
|
||||||
|
enter="transform ease-out duration-1300 transition"
|
||||||
|
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
|
||||||
|
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{renderIcon(currentNotification?.level)}
|
||||||
|
<div className="flex content-center flex-1 justify-between">
|
||||||
|
<p className="flex-1 text-sm font-medium text-gray-900 ml-2">{currentNotification?.message}</p>
|
||||||
|
{currentNotification?.actionButtonText ? <button
|
||||||
|
type="button"
|
||||||
|
className="ml-3 flex-shrink-0 rounded-md bg-white text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={() => handleOnClick()}
|
||||||
|
>
|
||||||
|
{currentNotification?.actionButtonText}
|
||||||
|
</button>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(dismissCurrentNotification())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification
|
||||||
87
frontend/components/utils/LanguageSelect.tsx
Normal file
87
frontend/components/utils/LanguageSelect.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Combobox } from '@headlessui/react'
|
||||||
|
import { LanguageIcon } from '@heroicons/react/20/solid'
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import classNames from '../../utils/classNames'
|
||||||
|
import getSupportedLanguages from '../../utils/getSupportedLanguages'
|
||||||
|
import { entities } from '../../wailsjs/wailsjs/go/models'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultLanguage?: entities.Language,
|
||||||
|
onSelect?: Function
|
||||||
|
styles?: Partial<React.CSSProperties>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageSelect = (props?: Props) => {
|
||||||
|
const [languages, setLanguages] = useState<entities.Language[]>([])
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState<entities.Language | undefined>(props?.defaultLanguage)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
|
|
||||||
|
const filteredLanguages = query !== ''
|
||||||
|
? languages.filter(l => l.displayName.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
: languages
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (languages.length === 0) {
|
||||||
|
getSupportedLanguages().then(response => {
|
||||||
|
setLanguages(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedLanguage(props?.defaultLanguage)
|
||||||
|
}, [props?.defaultLanguage])
|
||||||
|
|
||||||
|
const handleLanguageChange = (language: entities.Language) => {
|
||||||
|
if (props?.onSelect) props.onSelect(language)
|
||||||
|
setSelectedLanguage(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Combobox as="div" value={selectedLanguage} onChange={handleLanguageChange} className='block w-full'>
|
||||||
|
<div className="block relative">
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full border-none bg-white shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
displayValue={(language: entities.Language) => language?.displayName}
|
||||||
|
placeholder='Document Language'
|
||||||
|
style={props?.styles}
|
||||||
|
/>
|
||||||
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
|
||||||
|
<LanguageIcon className="text-gray-400" style={props?.styles ? {width: props.styles.fontSize} : {}} />
|
||||||
|
<ChevronUpDownIcon className=" text-gray-400" aria-hidden="true" style={props?.styles ? {width: props.styles.fontSize} : {}} />
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
{filteredLanguages.length > 0 && (
|
||||||
|
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
{filteredLanguages.map((l) => (
|
||||||
|
<Combobox.Option
|
||||||
|
style={props?.styles}
|
||||||
|
key={l.displayName}
|
||||||
|
value={l}
|
||||||
|
className={({ active }) => classNames(
|
||||||
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
|
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
|
||||||
|
)}>
|
||||||
|
{({ active, selected }) => <>
|
||||||
|
<span className={classNames('block truncate', selected && 'font-semibold')}>{l.displayName}</span>
|
||||||
|
{selected && (
|
||||||
|
<span className={classNames(
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||||
|
active ? 'text-white' : 'text-indigo-600'
|
||||||
|
)}>
|
||||||
|
<CheckIcon aria-hidden="true" style={props?.styles ? {width: props.styles.fontSize} : {}} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageSelect
|
||||||
@ -13,7 +13,9 @@ const MainWorkspace = () => {
|
|||||||
|
|
||||||
const renderSelectedWorkSpace = () => {
|
const renderSelectedWorkSpace = () => {
|
||||||
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
|
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
|
||||||
else return !selectedDocumentId ? <NoSelectedDocument /> : <DocumentCanvas />
|
else return !selectedDocumentId
|
||||||
|
? <NoSelectedDocument />
|
||||||
|
: <DocumentCanvas />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">
|
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { useProject } from '../../../context/Project/provider'
|
import { useProject } from '../../../context/Project/provider'
|
||||||
import classNames from '../../../utils/classNames'
|
import classNames from '../../../utils/classNames'
|
||||||
import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
import { ArrowPathIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||||
import { SidebarArea } from './types'
|
import { SidebarArea } from './types'
|
||||||
import { useSidebar } from './provider'
|
import { useSidebar } from './provider'
|
||||||
import onEnterHandler from '../../../utils/onEnterHandler'
|
import onEnterHandler from '../../../utils/onEnterHandler'
|
||||||
@ -15,13 +15,13 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
|
|||||||
getAreaById,
|
getAreaById,
|
||||||
requestUpdateArea,
|
requestUpdateArea,
|
||||||
setSelectedDocumentId,
|
setSelectedDocumentId,
|
||||||
setSelectedAreaId,
|
|
||||||
requestChangeAreaOrder,
|
requestChangeAreaOrder,
|
||||||
requestDeleteAreaById,
|
requestDeleteAreaById,
|
||||||
|
selectedAreaId,
|
||||||
|
setSelectedAreaId,
|
||||||
} = useProject()
|
} = useProject()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedAreaId,
|
|
||||||
isEditAreaNameInputShowing,
|
isEditAreaNameInputShowing,
|
||||||
setIsEditAreaNameInputShowing,
|
setIsEditAreaNameInputShowing,
|
||||||
dragOverAreaId,
|
dragOverAreaId,
|
||||||
@ -30,7 +30,6 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
|
|||||||
|
|
||||||
const editAreaNameTextInput = useRef<HTMLInputElement>(null)
|
const editAreaNameTextInput = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
|
||||||
const onConfirmAreaNameChangeHandler = async (areaDetails: { areaId: string, areaName: string }) => {
|
const onConfirmAreaNameChangeHandler = async (areaDetails: { areaId: string, areaName: string }) => {
|
||||||
const { areaId, areaName } = areaDetails
|
const { areaId, areaName } = areaDetails
|
||||||
|
|
||||||
@ -126,7 +125,7 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
onClick={handleReprocessAreaButtonClick}
|
onClick={handleReprocessAreaButtonClick}
|
||||||
/>
|
/>
|
||||||
<XMarkIcon
|
<TrashIcon
|
||||||
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
||||||
onClick={() => handleAreaDeleteButtonClick(props.area.id)} />
|
onClick={() => handleAreaDeleteButtonClick(props.area.id)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -135,7 +135,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
|
|||||||
props.document.id === selectedDocumentId
|
props.document.id === selectedDocumentId
|
||||||
? 'bg-gray-900 text-white'
|
? 'bg-gray-900 text-white'
|
||||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
||||||
'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 '
|
'text-left font-medium text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 inline-block'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.document.name}
|
{props.document.name}
|
||||||
@ -143,7 +143,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
className='w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
className='inline-block w-6 h-5 mr-2 text-white hover:bg-red-400 hover:text-gray-100 rounded-full p-0.5'
|
||||||
onClick={() => requestDeleteDocumentById(props.document.id)} />
|
onClick={() => requestDeleteDocumentById(props.document.id)} />
|
||||||
</summary>
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
7
frontend/consts/index.ts
Normal file
7
frontend/consts/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const colors = {
|
||||||
|
BRAND_PRIMARY: {
|
||||||
|
hex: '#dc8dec',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { colors }
|
||||||
@ -11,7 +11,7 @@ export function useNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = { children: ReactNode, navigationProps: NavigationProps }
|
type Props = { children: ReactNode, navigationProps: NavigationProps }
|
||||||
export function NavigationProvidor({ children, navigationProps }: Props) {
|
export function NavigationProvider({ children, navigationProps }: Props) {
|
||||||
const [selectedWorkspace, setSelectedWorkspace] = useState<workspaces>(navigationProps.selectedWorkspace)
|
const [selectedWorkspace, setSelectedWorkspace] = useState<workspaces>(navigationProps.selectedWorkspace)
|
||||||
const [selectedMainPage, setSelectedMainPage] = useState<mainPages>(navigationProps.selectedMainPage)
|
const [selectedMainPage, setSelectedMainPage] = useState<mainPages>(navigationProps.selectedMainPage)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { saveDocuments } from '../../useCases/saveData'
|
import { saveDocuments } from '../../useCases/saveData'
|
||||||
import { GetProcessedAreasByDocumentId, RequestAddArea, RequestAddProcessedArea, RequestChangeAreaOrder, RequestDeleteAreaById, RequestUpdateArea } from '../../wailsjs/wailsjs/go/ipc/Channel'
|
import { GetProcessedAreasByDocumentId, RequestAddArea, RequestAddProcessedArea, RequestChangeAreaOrder, RequestDeleteAreaById, RequestUpdateArea, RequestUpdateProcessedArea, } from '../../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
import { entities, ipc } from '../../wailsjs/wailsjs/go/models'
|
import { entities, ipc } from '../../wailsjs/wailsjs/go/models'
|
||||||
import { AddAreaProps, AreaProps } from './types'
|
import { AddAreaProps, AreaProps } from './types'
|
||||||
|
|
||||||
@ -45,12 +45,13 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUpdateArea = async (updatedArea: AreaProps): Promise<entities.Area> => {
|
const requestUpdateArea = async (updatedArea: AreaProps): Promise<boolean> => {
|
||||||
const response = await RequestUpdateArea(new entities.Area(updatedArea))
|
console.log('requestUpdateArea', updatedArea)
|
||||||
|
const wasSuccessful = await RequestUpdateArea(new entities.Area(updatedArea))
|
||||||
|
|
||||||
if (response.id) await updateDocuments()
|
if (wasSuccessful) await updateDocuments()
|
||||||
saveDocuments()
|
saveDocuments()
|
||||||
return response
|
return wasSuccessful
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestDeleteAreaById = async (areaId: string): Promise<boolean> => {
|
const requestDeleteAreaById = async (areaId: string): Promise<boolean> => {
|
||||||
@ -62,6 +63,8 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
|
|||||||
|
|
||||||
const requestAddProcessedArea = async (processedArea: entities.ProcessedArea) => await RequestAddProcessedArea(processedArea)
|
const requestAddProcessedArea = async (processedArea: entities.ProcessedArea) => await RequestAddProcessedArea(processedArea)
|
||||||
|
|
||||||
|
const requestUpdateProcessedArea = async (updatedProcessedArea: entities.ProcessedArea) => await RequestUpdateProcessedArea(updatedProcessedArea)
|
||||||
|
|
||||||
const requestChangeAreaOrder = async (areaId: string, newOrder: number) => {
|
const requestChangeAreaOrder = async (areaId: string, newOrder: number) => {
|
||||||
const response = await RequestChangeAreaOrder(areaId, newOrder)
|
const response = await RequestChangeAreaOrder(areaId, newOrder)
|
||||||
await updateDocuments()
|
await updateDocuments()
|
||||||
@ -76,6 +79,7 @@ const createAreaProviderMethods = (dependencies: Dependencies) => {
|
|||||||
requestDeleteAreaById,
|
requestDeleteAreaById,
|
||||||
getProcessedAreasByDocumentId,
|
getProcessedAreasByDocumentId,
|
||||||
requestAddProcessedArea,
|
requestAddProcessedArea,
|
||||||
|
requestUpdateProcessedArea,
|
||||||
requestChangeAreaOrder,
|
requestChangeAreaOrder,
|
||||||
getProcessedAreaById,
|
getProcessedAreaById,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { saveContextGroups } from '../../useCases/saveData'
|
||||||
|
import { RequestConnectProcessedAreas, GetSerializedContextGroups } from '../../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
|
import { entities } from '../../wailsjs/wailsjs/go/models'
|
||||||
|
|
||||||
|
|
||||||
|
type Dependencies = { updateDocuments: Function }
|
||||||
|
|
||||||
|
const createContextGroupProviderMethods = (dependencies?: Dependencies) => {
|
||||||
|
|
||||||
|
const requestConnectProcessedAreas = async (headId: string, tailId: string) => {
|
||||||
|
let wasSuccessful = false
|
||||||
|
try {
|
||||||
|
wasSuccessful = await RequestConnectProcessedAreas(headId, tailId)
|
||||||
|
await saveContextGroups()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
dependencies?.updateDocuments()
|
||||||
|
return wasSuccessful
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSerializedContextGroups = async () => {
|
||||||
|
let response: entities.SerializedLinkedProcessedArea[] = []
|
||||||
|
try {
|
||||||
|
response = await GetSerializedContextGroups()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestConnectProcessedAreas,
|
||||||
|
getSerializedContextGroups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createContextGroupProviderMethods
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { saveUserProcessedMarkdown } from '../../useCases/saveData'
|
import { saveUserProcessedMarkdown } from '../../useCases/saveData'
|
||||||
import { GetUserMarkdownByDocumentId, RequestUpdateDocumentUserMarkdown } from '../../wailsjs/wailsjs/go/ipc/Channel'
|
import { GetUserMarkdownByDocumentId, RequestUpdateDocumentUserMarkdown } from '../../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
import { ipc, entities } from '../../wailsjs/wailsjs/go/models'
|
import { entities } from '../../wailsjs/wailsjs/go/models'
|
||||||
|
|
||||||
type Dependencies = {}
|
type Dependencies = {}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { entities } from '../../wailsjs/wailsjs/go/models'
|
import { entities, ipc } from '../../wailsjs/wailsjs/go/models'
|
||||||
import { ProjectContextType, UserProps } from './types'
|
import { ProjectContextType, UserProps } from './types'
|
||||||
|
|
||||||
const makeDefaultProject = (): ProjectContextType => ({
|
const makeDefaultProject = (): ProjectContextType => ({
|
||||||
id: '',
|
id: '',
|
||||||
documents: [] as entities.Document[],
|
documents: [] as entities.Document[],
|
||||||
groups: [] as entities.Group[],
|
groups: [] as entities.Group[],
|
||||||
|
contextGroups: [] as entities.SerializedLinkedProcessedArea[],
|
||||||
selectedAreaId: '',
|
selectedAreaId: '',
|
||||||
selectedDocumentId: '',
|
selectedDocumentId: '',
|
||||||
getSelectedDocument: () => new entities.Document(),
|
getSelectedDocument: () => new entities.Document(),
|
||||||
@ -12,7 +13,7 @@ const makeDefaultProject = (): ProjectContextType => ({
|
|||||||
getProcessedAreasByDocumentId: (documentId) => Promise.resolve([new entities.ProcessedArea()]),
|
getProcessedAreasByDocumentId: (documentId) => Promise.resolve([new entities.ProcessedArea()]),
|
||||||
requestAddProcessedArea: (processesArea) => Promise.resolve(new entities.ProcessedArea()),
|
requestAddProcessedArea: (processesArea) => Promise.resolve(new entities.ProcessedArea()),
|
||||||
requestAddArea: (documentId, area) => Promise.resolve(new entities.Area()),
|
requestAddArea: (documentId, area) => Promise.resolve(new entities.Area()),
|
||||||
requestUpdateArea: (updatedArea) => Promise.resolve(new entities.Area()),
|
requestUpdateArea: (updatedArea) => Promise.resolve(false),
|
||||||
requestDeleteAreaById: (areaId) => Promise.resolve(false),
|
requestDeleteAreaById: (areaId) => Promise.resolve(false),
|
||||||
requestAddDocument: (groupId, documentName) => Promise.resolve(new entities.Document()),
|
requestAddDocument: (groupId, documentName) => Promise.resolve(new entities.Document()),
|
||||||
requestDeleteDocumentById: (documentId) => Promise.resolve(false),
|
requestDeleteDocumentById: (documentId) => Promise.resolve(false),
|
||||||
@ -32,6 +33,10 @@ const makeDefaultProject = (): ProjectContextType => ({
|
|||||||
requestSelectProjectByName: (projectName) => Promise.resolve(false),
|
requestSelectProjectByName: (projectName) => Promise.resolve(false),
|
||||||
requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false),
|
requestUpdateProcessedWordById: (wordId, newTestValue) => Promise.resolve(false),
|
||||||
getProcessedAreaById: (areaId) => Promise.resolve(undefined),
|
getProcessedAreaById: (areaId) => Promise.resolve(undefined),
|
||||||
|
requestUpdateProcessedArea: updatedProcessedArea => Promise.resolve(false),
|
||||||
|
requestConnectProcessedAreas: (headId, tailId) => Promise.resolve(false),
|
||||||
|
getSerializedContextGroups: () => Promise.resolve([]),
|
||||||
|
updateDocuments: () => Promise.resolve(new ipc.GetDocumentsResponse())
|
||||||
})
|
})
|
||||||
|
|
||||||
export default makeDefaultProject
|
export default makeDefaultProject
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import createAreaProviderMethods from './createAreaProviderMethods'
|
|||||||
import createDocumentProviderMethods from './createDocumentMethods'
|
import createDocumentProviderMethods from './createDocumentMethods'
|
||||||
import createSessionProviderMethods from './createSessionProviderMethods'
|
import createSessionProviderMethods from './createSessionProviderMethods'
|
||||||
import createUserMarkdownProviderMethods from './createUserMarkdownProviderMethods'
|
import createUserMarkdownProviderMethods from './createUserMarkdownProviderMethods'
|
||||||
|
import createContextGroupProviderMethods from './createContextGroupProviderMethods'
|
||||||
|
|
||||||
const ProjectContext = createContext<ProjectContextType>(makeDefaultProject())
|
const ProjectContext = createContext<ProjectContextType>(makeDefaultProject())
|
||||||
|
|
||||||
@ -21,15 +22,17 @@ type Props = { children: ReactNode, projectProps: ProjectProps }
|
|||||||
export function ProjectProvider({ children, projectProps }: Props) {
|
export function ProjectProvider({ children, projectProps }: Props) {
|
||||||
const [documents, setDocuments] = useState<entities.Document[]>(projectProps.documents)
|
const [documents, setDocuments] = useState<entities.Document[]>(projectProps.documents)
|
||||||
const [groups, setGroups] = useState<entities.Group[]>(projectProps.groups)
|
const [groups, setGroups] = useState<entities.Group[]>(projectProps.groups)
|
||||||
|
const [contextGroups, setContextGroups] = useState<entities.SerializedLinkedProcessedArea[]>(projectProps.contextGroups)
|
||||||
const [selectedAreaId, setSelectedAreaId] = useState<string>('')
|
const [selectedAreaId, setSelectedAreaId] = useState<string>('')
|
||||||
const [selectedDocumentId, setSelectedDocumentId] = useState<string>('')
|
const [selectedDocumentId, setSelectedDocumentId] = useState<string>('')
|
||||||
const [currentSession, setCurrentSession] = useState<entities.Session>(new entities.Session())
|
const [currentSession, setCurrentSession] = useState<entities.Session>(new entities.Session())
|
||||||
|
|
||||||
const updateDocuments = async () => {
|
const updateDocuments = async () => {
|
||||||
const response = await GetDocuments()
|
const response = await GetDocuments()
|
||||||
const { documents, groups } = response
|
const { documents, groups, contextGroups } = response
|
||||||
setDocuments(documents)
|
setDocuments(documents)
|
||||||
setGroups(groups)
|
setGroups(groups)
|
||||||
|
setContextGroups(contextGroups)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
|||||||
const areaMethods = createAreaProviderMethods({ documents, updateDocuments, selectedDocumentId })
|
const areaMethods = createAreaProviderMethods({ documents, updateDocuments, selectedDocumentId })
|
||||||
const sessionMethods = createSessionProviderMethods({ updateSession, updateDocuments })
|
const sessionMethods = createSessionProviderMethods({ updateSession, updateDocuments })
|
||||||
const userMarkDownMethods = createUserMarkdownProviderMethods()
|
const userMarkDownMethods = createUserMarkdownProviderMethods()
|
||||||
|
const contextGroupMethods = createContextGroupProviderMethods({ updateDocuments })
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -60,15 +64,18 @@ export function ProjectProvider({ children, projectProps }: Props) {
|
|||||||
id: '',
|
id: '',
|
||||||
documents,
|
documents,
|
||||||
groups,
|
groups,
|
||||||
|
contextGroups,
|
||||||
selectedAreaId,
|
selectedAreaId,
|
||||||
setSelectedAreaId,
|
setSelectedAreaId,
|
||||||
selectedDocumentId,
|
selectedDocumentId,
|
||||||
setSelectedDocumentId,
|
setSelectedDocumentId,
|
||||||
currentSession,
|
currentSession,
|
||||||
|
updateDocuments,
|
||||||
...areaMethods,
|
...areaMethods,
|
||||||
...documentMethods,
|
...documentMethods,
|
||||||
...sessionMethods,
|
...sessionMethods,
|
||||||
...userMarkDownMethods,
|
...userMarkDownMethods,
|
||||||
|
...contextGroupMethods,
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProjectContext.Provider value={value}>
|
return <ProjectContext.Provider value={value}>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export type ProjectProps = {
|
|||||||
id: string,
|
id: string,
|
||||||
documents: entities.Document[],
|
documents: entities.Document[],
|
||||||
groups: entities.Group[],
|
groups: entities.Group[],
|
||||||
|
contextGroups: entities.SerializedLinkedProcessedArea[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddAreaProps = {
|
export type AddAreaProps = {
|
||||||
@ -42,7 +43,7 @@ export type ProjectContextType = {
|
|||||||
getProcessedAreasByDocumentId: (documentId: string) => Promise<entities.ProcessedArea[]>
|
getProcessedAreasByDocumentId: (documentId: string) => Promise<entities.ProcessedArea[]>
|
||||||
requestAddProcessedArea: (processedArea: entities.ProcessedArea) => Promise<entities.ProcessedArea>
|
requestAddProcessedArea: (processedArea: entities.ProcessedArea) => Promise<entities.ProcessedArea>
|
||||||
requestAddArea: (documentId: string, area: AddAreaProps) => Promise<entities.Area>
|
requestAddArea: (documentId: string, area: AddAreaProps) => Promise<entities.Area>
|
||||||
requestUpdateArea: (area: AreaProps) => Promise<entities.Area>
|
requestUpdateArea: (area: AreaProps) => Promise<boolean>
|
||||||
requestDeleteAreaById: (areaId: string) => Promise<boolean>
|
requestDeleteAreaById: (areaId: string) => Promise<boolean>
|
||||||
requestAddDocument: (groupId: string, documentName: string) => Promise<entities.Document>
|
requestAddDocument: (groupId: string, documentName: string) => Promise<entities.Document>
|
||||||
requestDeleteDocumentById: (documentId: string) => Promise<boolean>
|
requestDeleteDocumentById: (documentId: string) => Promise<boolean>
|
||||||
@ -64,4 +65,8 @@ export type ProjectContextType = {
|
|||||||
requestSelectProjectByName: (projectName: string) => Promise<boolean>
|
requestSelectProjectByName: (projectName: string) => Promise<boolean>
|
||||||
requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise<boolean>
|
requestUpdateProcessedWordById: (wordId: string, newTextValue: string) => Promise<boolean>
|
||||||
getProcessedAreaById: (areaId: string) => Promise<entities.ProcessedArea | undefined>
|
getProcessedAreaById: (areaId: string) => Promise<entities.ProcessedArea | undefined>
|
||||||
|
requestUpdateProcessedArea: (updatedProcessedArea: entities.ProcessedArea) => Promise<boolean>
|
||||||
|
requestConnectProcessedAreas: (headId: string, tailId: string) => Promise<boolean>
|
||||||
|
getSerializedContextGroups: () => Promise<entities.SerializedLinkedProcessedArea[]>
|
||||||
|
updateDocuments: () => Promise<ipc.GetDocumentsResponse>
|
||||||
} & ProjectProps
|
} & ProjectProps
|
||||||
217
frontend/package-lock.json
generated
217
frontend/package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"@headlessui/react": "^1.7.4",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@heroicons/react": "^2.0.13",
|
"@heroicons/react": "^2.0.13",
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"konva": "^9.2.0",
|
"konva": "^9.2.0",
|
||||||
"next": "^13.4.4",
|
"next": "^13.4.4",
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"react-konva": "^18.2.9",
|
"react-konva": "^18.2.9",
|
||||||
"react-konva-utils": "^1.0.4",
|
"react-konva-utils": "^1.0.4",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
|
"react-redux": "^8.1.2",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"tesseract.js": "^4.0.2",
|
"tesseract.js": "^4.0.2",
|
||||||
"use-image": "^1.1.0",
|
"use-image": "^1.1.0",
|
||||||
@ -214,7 +216,6 @@
|
|||||||
"version": "7.20.7",
|
"version": "7.20.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
||||||
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
},
|
},
|
||||||
@ -633,6 +634,29 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"url": "https://opencollective.com/unts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "1.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
|
||||||
|
"integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^9.0.21",
|
||||||
|
"redux": "^4.2.1",
|
||||||
|
"redux-thunk": "^2.4.2",
|
||||||
|
"reselect": "^4.1.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18",
|
||||||
|
"react-redux": "^7.2.1 || ^8.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rushstack/eslint-patch": {
|
"node_modules/@rushstack/eslint-patch": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
||||||
@ -674,6 +698,15 @@
|
|||||||
"@types/unist": "*"
|
"@types/unist": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
@ -723,7 +756,7 @@
|
|||||||
"version": "18.0.10",
|
"version": "18.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
|
||||||
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
|
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@ -746,6 +779,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
|
||||||
|
},
|
||||||
"node_modules/@types/uuid": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "8.3.4",
|
"version": "8.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
@ -2588,6 +2626,14 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-void-elements": {
|
"node_modules/html-void-elements": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
@ -2622,6 +2668,15 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "9.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
|
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@ -4433,6 +4488,49 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/use-sync-external-store": "^0.0.3",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
|
"use-sync-external-store": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-native": ">=0.59",
|
||||||
|
"redux": "^4 || ^5.0.0-beta.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-redux/node_modules/react-is": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@ -4452,6 +4550,22 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regenerator-runtime": {
|
"node_modules/regenerator-runtime": {
|
||||||
"version": "0.13.11",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
@ -4528,6 +4642,11 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.1",
|
"version": "1.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
@ -5288,6 +5407,14 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -5630,7 +5757,6 @@
|
|||||||
"version": "7.20.7",
|
"version": "7.20.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz",
|
||||||
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
"integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.11"
|
"regenerator-runtime": "^0.13.11"
|
||||||
}
|
}
|
||||||
@ -5895,6 +6021,17 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@reduxjs/toolkit": {
|
||||||
|
"version": "1.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
|
||||||
|
"integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==",
|
||||||
|
"requires": {
|
||||||
|
"immer": "^9.0.21",
|
||||||
|
"redux": "^4.2.1",
|
||||||
|
"redux-thunk": "^2.4.2",
|
||||||
|
"reselect": "^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@rushstack/eslint-patch": {
|
"@rushstack/eslint-patch": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
||||||
@ -5933,6 +6070,15 @@
|
|||||||
"@types/unist": "*"
|
"@types/unist": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/json5": {
|
"@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
@ -5982,7 +6128,7 @@
|
|||||||
"version": "18.0.10",
|
"version": "18.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
|
||||||
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
|
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
@ -6005,6 +6151,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
|
"@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
|
||||||
|
},
|
||||||
"@types/uuid": {
|
"@types/uuid": {
|
||||||
"version": "8.3.4",
|
"version": "8.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
@ -7317,6 +7468,14 @@
|
|||||||
"space-separated-tokens": "^2.0.0"
|
"space-separated-tokens": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"requires": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"html-void-elements": {
|
"html-void-elements": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
@ -7341,6 +7500,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||||
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
|
||||||
},
|
},
|
||||||
|
"immer": {
|
||||||
|
"version": "9.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||||
|
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
|
||||||
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@ -8446,6 +8610,26 @@
|
|||||||
"scheduler": "^0.23.0"
|
"scheduler": "^0.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
"@types/use-sync-external-store": "^0.0.3",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
|
"use-sync-external-store": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"read-cache": {
|
"read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@ -8462,6 +8646,20 @@
|
|||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redux-thunk": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.11",
|
"version": "0.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
@ -8514,6 +8712,11 @@
|
|||||||
"unified": "^10.0.0"
|
"unified": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reselect": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.22.1",
|
"version": "1.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
@ -9045,6 +9248,12 @@
|
|||||||
"integrity": "sha512-+cBHRR/44ZyMUS873O0vbVylgMM0AbdTunEplAWXvIQ2p69h2sIo2Qq74zeUsq6AMo+27e5lERQvXzd1crGiMg==",
|
"integrity": "sha512-+cBHRR/44ZyMUS873O0vbVylgMM0AbdTunEplAWXvIQ2p69h2sIo2Qq74zeUsq6AMo+27e5lERQvXzd1crGiMg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"@headlessui/react": "^1.7.4",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@heroicons/react": "^2.0.13",
|
"@heroicons/react": "^2.0.13",
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"konva": "^9.2.0",
|
"konva": "^9.2.0",
|
||||||
"next": "^13.4.4",
|
"next": "^13.4.4",
|
||||||
@ -24,6 +25,7 @@
|
|||||||
"react-konva": "^18.2.9",
|
"react-konva": "^18.2.9",
|
||||||
"react-konva-utils": "^1.0.4",
|
"react-konva-utils": "^1.0.4",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
|
"react-redux": "^8.1.2",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"tesseract.js": "^4.0.2",
|
"tesseract.js": "^4.0.2",
|
||||||
"use-image": "^1.1.0",
|
"use-image": "^1.1.0",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
e331f957a49840160190db6ea894d0b5
|
bf8d6eeb2add78baa4092415a836f1ad
|
||||||
@ -5,8 +5,9 @@ import { ProjectProvider } from '../context/Project/provider'
|
|||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
import { entities } from '../wailsjs/wailsjs/go/models'
|
import { entities } from '../wailsjs/wailsjs/go/models'
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
import { NavigationProvidor } from '../context/Navigation/provider'
|
import { NavigationProvider } from '../context/Navigation/provider'
|
||||||
import { mainPages, workspaces } from '../context/Navigation/types'
|
import { mainPages, workspaces } from '../context/Navigation/types'
|
||||||
|
import { Providers } from '../redux/provider'
|
||||||
|
|
||||||
const initialProjectProps = {
|
const initialProjectProps = {
|
||||||
id: '',
|
id: '',
|
||||||
@ -21,10 +22,12 @@ const initialNavigationProps = {
|
|||||||
|
|
||||||
export default function MainAppLayout({ Component, pageProps }: AppProps) {
|
export default function MainAppLayout({ Component, pageProps }: AppProps) {
|
||||||
return <div className='min-h-screen' >
|
return <div className='min-h-screen' >
|
||||||
<NavigationProvidor navigationProps={initialNavigationProps}>
|
<NavigationProvider navigationProps={initialNavigationProps}>
|
||||||
<ProjectProvider projectProps={initialProjectProps}>
|
<ProjectProvider projectProps={initialProjectProps}>
|
||||||
|
<Providers>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</Providers>
|
||||||
</ProjectProvider>
|
</ProjectProvider>
|
||||||
</NavigationProvidor>
|
</NavigationProvider>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import MainHead from '../components/head'
|
import MainHead from '../components/head'
|
||||||
import MainProject from '../components/project/Main'
|
import MainProject from '../components/project/Main'
|
||||||
import User from '../components/settings/User'
|
import User from '../components/settings/User'
|
||||||
@ -8,6 +7,7 @@ import Navigation from '../components/workspace/Navigation'
|
|||||||
import { useNavigation } from '../context/Navigation/provider'
|
import { useNavigation } from '../context/Navigation/provider'
|
||||||
import { mainPages } from '../context/Navigation/types'
|
import { mainPages } from '../context/Navigation/types'
|
||||||
import { useProject } from '../context/Project/provider'
|
import { useProject } from '../context/Project/provider'
|
||||||
|
import Notification from '../components/Notifications'
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
const { currentSession } = useProject()
|
const { currentSession } = useProject()
|
||||||
@ -28,6 +28,7 @@ const Home: NextPage = () => {
|
|||||||
return <>
|
return <>
|
||||||
<MainHead />
|
<MainHead />
|
||||||
{renderSelectedMainPage()}
|
{renderSelectedMainPage()}
|
||||||
|
<Notification />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { NotificationProps, NotificationQueueState } from './types'
|
||||||
|
|
||||||
|
const initialState: NotificationQueueState = {
|
||||||
|
currentNotification: undefined,
|
||||||
|
queue: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationQueueSlice = createSlice({
|
||||||
|
name: 'notifications',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setNotifications: (state, action: PayloadAction<NotificationProps[]>) => {
|
||||||
|
state.queue = action.payload
|
||||||
|
},
|
||||||
|
setCurrentNotification: (state, action: PayloadAction<NotificationProps | undefined>) => {
|
||||||
|
state.currentNotification = action.payload
|
||||||
|
},
|
||||||
|
pushNotification: (state, action: PayloadAction<NotificationProps>) => {
|
||||||
|
let { queue } = state
|
||||||
|
const { payload: newNotification } = action
|
||||||
|
|
||||||
|
if (queue.length) queue.push(newNotification)
|
||||||
|
else {
|
||||||
|
queue.push(newNotification)
|
||||||
|
state.currentNotification = newNotification
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissCurrentNotification: (state) => {
|
||||||
|
state.queue.shift()
|
||||||
|
state.currentNotification = state.queue[0] || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setNotifications,
|
||||||
|
setCurrentNotification,
|
||||||
|
pushNotification,
|
||||||
|
dismissCurrentNotification
|
||||||
|
} = notificationQueueSlice.actions
|
||||||
|
|
||||||
|
export default notificationQueueSlice.reducer
|
||||||
15
frontend/redux/features/notifications/types.ts
Normal file
15
frontend/redux/features/notifications/types.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export type NotificationLevel = 'info' | 'warning' | 'error'
|
||||||
|
|
||||||
|
export type NotificationProps = {
|
||||||
|
shouldShow?: boolean,
|
||||||
|
message: string,
|
||||||
|
actionButtonText?: string,
|
||||||
|
onActionClickCallback?: Function,
|
||||||
|
closeOnAction?: boolean,
|
||||||
|
level?: NotificationLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationQueueState = {
|
||||||
|
queue: NotificationProps[],
|
||||||
|
currentNotification?: NotificationProps
|
||||||
|
}
|
||||||
66
frontend/redux/features/stage/stageSlice.ts
Normal file
66
frontend/redux/features/stage/stageSlice.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { ContextConnectionPoint, StageState } from './types'
|
||||||
|
|
||||||
|
export const maxScale = 4
|
||||||
|
export const scaleStep = 0.01
|
||||||
|
|
||||||
|
const initialState: StageState = {
|
||||||
|
size: { width: 1, height: 1 },
|
||||||
|
scale: 1,
|
||||||
|
areAreasVisible: true,
|
||||||
|
areProcessedWordsVisible: true,
|
||||||
|
areTranslatedWordsVisible: false,
|
||||||
|
areLinkAreaContextsVisible: false,
|
||||||
|
isDrawingArea: false,
|
||||||
|
startingContextConnectionPoint: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stageSlice = createSlice({
|
||||||
|
name: 'stage',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSize: (state, action: PayloadAction<{width: number, height: number}>) => {
|
||||||
|
state.size = action.payload
|
||||||
|
},
|
||||||
|
setScale: (state, action: PayloadAction<number>) => {
|
||||||
|
let clampedScale = action.payload
|
||||||
|
|
||||||
|
if (clampedScale > maxScale) clampedScale = maxScale
|
||||||
|
else if (clampedScale < scaleStep) clampedScale = scaleStep
|
||||||
|
|
||||||
|
state.scale = clampedScale
|
||||||
|
},
|
||||||
|
setAreAreasVisible: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.areAreasVisible = action.payload
|
||||||
|
},
|
||||||
|
setAreProcessedWordsVisible: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.areProcessedWordsVisible = action.payload
|
||||||
|
},
|
||||||
|
setAreTranslatedWordsVisible: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.areTranslatedWordsVisible = action.payload
|
||||||
|
},
|
||||||
|
setAreLinkAreaContextsVisible: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.areLinkAreaContextsVisible = action.payload
|
||||||
|
},
|
||||||
|
setIsDrawingArea: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isDrawingArea = action.payload
|
||||||
|
},
|
||||||
|
setStartingContextConnectionPoint: (state, action: PayloadAction<ContextConnectionPoint | null>) => {
|
||||||
|
state.startingContextConnectionPoint = action.payload
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSize,
|
||||||
|
setScale,
|
||||||
|
setAreAreasVisible,
|
||||||
|
setAreProcessedWordsVisible,
|
||||||
|
setAreTranslatedWordsVisible,
|
||||||
|
setAreLinkAreaContextsVisible,
|
||||||
|
setIsDrawingArea,
|
||||||
|
setStartingContextConnectionPoint,
|
||||||
|
} = stageSlice.actions
|
||||||
|
|
||||||
|
export default stageSlice.reducer
|
||||||
15
frontend/redux/features/stage/types.ts
Normal file
15
frontend/redux/features/stage/types.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export type ContextConnectionPoint = {
|
||||||
|
isHead: boolean,
|
||||||
|
areaId: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StageState = {
|
||||||
|
size: { width: number, height: number },
|
||||||
|
scale: number,
|
||||||
|
areAreasVisible: boolean,
|
||||||
|
areProcessedWordsVisible: boolean,
|
||||||
|
areTranslatedWordsVisible: boolean,
|
||||||
|
areLinkAreaContextsVisible: boolean,
|
||||||
|
isDrawingArea: boolean,
|
||||||
|
startingContextConnectionPoint: ContextConnectionPoint | null
|
||||||
|
}
|
||||||
5
frontend/redux/hooks.ts
Normal file
5
frontend/redux/hooks.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||||
|
import type { RootState, AppDispatch } from './store'
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
8
frontend/redux/provider.tsx
Normal file
8
frontend/redux/provider.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { store } from './store'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return <Provider store={store}>{children}</Provider>
|
||||||
|
}
|
||||||
18
frontend/redux/store.ts
Normal file
18
frontend/redux/store.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
import { setupListeners } from '@reduxjs/toolkit/dist/query'
|
||||||
|
import notificationQueueSlice from './features/notifications/notificationQueueSlice'
|
||||||
|
import stageSlice from './features/stage/stageSlice'
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
notificationQueue: notificationQueueSlice,
|
||||||
|
stage: stageSlice,
|
||||||
|
},
|
||||||
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
|
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
|
||||||
|
setupListeners(store.dispatch)
|
||||||
@ -16,4 +16,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
colors: {
|
||||||
|
brandPrimary: '#dc8dec',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { createScheduler, createWorker } from 'tesseract.js'
|
import { createScheduler, createWorker, PSM } from 'tesseract.js'
|
||||||
import { GetAreaById, GetDocumentById, RequestAddProcessedArea, RequestSaveProcessedTextCollection } from '../wailsjs/wailsjs/go/ipc/Channel'
|
import { GetAreaById, GetDocumentById, GetProcessedAreaById, RequestAddProcessedArea, RequestSaveProcessedTextCollection, RequestUpdateProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
import { entities } from '../wailsjs/wailsjs/go/models'
|
import { entities } from '../wailsjs/wailsjs/go/models'
|
||||||
import loadImage from './loadImage'
|
import loadImage from './loadImage'
|
||||||
import { saveProcessedText } from './saveData'
|
import { saveProcessedText } from './saveData'
|
||||||
@ -9,7 +9,9 @@ const processImageArea = async (documentId: string, areaId: string) => {
|
|||||||
const foundArea = await GetAreaById(areaId)
|
const foundArea = await GetAreaById(areaId)
|
||||||
if (!foundDocument.path || !foundDocument.areas?.length || !foundArea.id) return
|
if (!foundDocument.path || !foundDocument.areas?.length || !foundArea.id) return
|
||||||
|
|
||||||
const processLanguage = foundDocument.defaultLanguage.processCode
|
console.log(foundArea)
|
||||||
|
|
||||||
|
const processLanguage = foundArea.language.processCode || foundDocument.defaultLanguage.processCode
|
||||||
|
|
||||||
if (!processLanguage) return console.error('No process language selected')
|
if (!processLanguage) return console.error('No process language selected')
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ const processImageArea = async (documentId: string, areaId: string) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const addProcessesAreaRequest = await RequestAddProcessedArea(new entities.ProcessedArea({
|
const newProcessedArea = new entities.ProcessedArea({
|
||||||
id: foundArea.id,
|
id: foundArea.id,
|
||||||
documentId,
|
documentId,
|
||||||
order: foundArea.order,
|
order: foundArea.order,
|
||||||
@ -49,6 +51,7 @@ const processImageArea = async (documentId: string, areaId: string) => {
|
|||||||
lines: result.data.lines.map((l: any) => new entities.ProcessedLine({
|
lines: result.data.lines.map((l: any) => new entities.ProcessedLine({
|
||||||
fullText: l.text,
|
fullText: l.text,
|
||||||
words: l.words.map((w: any) => new entities.ProcessedWord({
|
words: l.words.map((w: any) => new entities.ProcessedWord({
|
||||||
|
areaId: foundArea.id,
|
||||||
fullText: w.text,
|
fullText: w.text,
|
||||||
direction: w.direction,
|
direction: w.direction,
|
||||||
confidence: w.confidence,
|
confidence: w.confidence,
|
||||||
@ -70,11 +73,22 @@ const processImageArea = async (documentId: string, areaId: string) => {
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
console.log(newProcessedArea)
|
||||||
|
|
||||||
|
|
||||||
|
const existingProcessedArea = await GetProcessedAreaById(areaId)
|
||||||
|
let didSuccessfullyProcess: boolean // TODO: fix this: this no longer is truthful, returns true or false if there was not a JS error
|
||||||
|
try {
|
||||||
|
if (existingProcessedArea.id !== areaId) await RequestAddProcessedArea(newProcessedArea)
|
||||||
|
else await RequestUpdateProcessedArea(newProcessedArea)
|
||||||
saveProcessedText()
|
saveProcessedText()
|
||||||
|
didSuccessfullyProcess = true
|
||||||
return addProcessesAreaRequest
|
} catch (err) {
|
||||||
|
didSuccessfullyProcess = false
|
||||||
|
}
|
||||||
|
return didSuccessfullyProcess
|
||||||
}
|
}
|
||||||
|
|
||||||
export default processImageArea
|
export default processImageArea
|
||||||
|
|||||||
103
frontend/useCases/processImageRect.ts
Normal file
103
frontend/useCases/processImageRect.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { PSM, createScheduler, createWorker } from 'tesseract.js'
|
||||||
|
import { GetDocumentById, RequestAddArea, RequestAddProcessedArea } from '../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
|
import loadImage from './loadImage'
|
||||||
|
import { entities } from '../wailsjs/wailsjs/go/models'
|
||||||
|
import { saveProcessedText } from './saveData'
|
||||||
|
|
||||||
|
type rect = {
|
||||||
|
startX: number,
|
||||||
|
endX: number,
|
||||||
|
startY: number,
|
||||||
|
endY: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const processImageRect = async (documentId: string, rectangle: rect): Promise<entities.ProcessedArea[]> => {
|
||||||
|
const foundDocument = await GetDocumentById(documentId)
|
||||||
|
const { path, defaultLanguage } = foundDocument
|
||||||
|
if (!path || !defaultLanguage) return []
|
||||||
|
|
||||||
|
const processLanguage = defaultLanguage.processCode
|
||||||
|
const imageData = await loadImage(path)
|
||||||
|
|
||||||
|
let workerOptions: Partial<Tesseract.WorkerOptions> = {}
|
||||||
|
if (foundDocument.defaultLanguage.isBundledCustom) {
|
||||||
|
workerOptions = {
|
||||||
|
langPath: '/customLanguages',
|
||||||
|
gzip: false,
|
||||||
|
// logger: m => console.log(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await createWorker(workerOptions)
|
||||||
|
await worker.loadLanguage(processLanguage)
|
||||||
|
await worker.initialize(processLanguage)
|
||||||
|
await worker.setParameters({
|
||||||
|
tessedit_pageseg_mode: PSM.AUTO_OSD,
|
||||||
|
})
|
||||||
|
|
||||||
|
const scheduler = createScheduler()
|
||||||
|
scheduler.addWorker(worker)
|
||||||
|
|
||||||
|
const result = await scheduler.addJob('recognize', imageData, {
|
||||||
|
rectangle: {
|
||||||
|
left: rectangle.startX,
|
||||||
|
top: rectangle.startY,
|
||||||
|
width: rectangle.endX - rectangle.startX,
|
||||||
|
height: rectangle.endY - rectangle.startY,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addAreaRequests = result.data.paragraphs.map(async (p: any) => {
|
||||||
|
const defaultAreaName = p.lines[0]?.words[0]?.text || ''
|
||||||
|
const area = await RequestAddArea(
|
||||||
|
documentId,
|
||||||
|
new entities.Area({
|
||||||
|
name: defaultAreaName,
|
||||||
|
startX: p.bbox.x0,
|
||||||
|
endX: p.bbox.x1,
|
||||||
|
startY: p.bbox.y0,
|
||||||
|
endY: p.bbox.y1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const processedArea = await RequestAddProcessedArea(new entities.ProcessedArea({
|
||||||
|
id: area.id,
|
||||||
|
documentId,
|
||||||
|
order: area.order,
|
||||||
|
fullText: p.text,
|
||||||
|
lines: p.lines.map((l: any) => new entities.ProcessedLine({
|
||||||
|
fullText: l.text,
|
||||||
|
words: l.words.map((w: any) => new entities.ProcessedWord({
|
||||||
|
areaId: area.id,
|
||||||
|
fullText: w.text,
|
||||||
|
direction: w.direction,
|
||||||
|
confidence: w.confidence,
|
||||||
|
boundingBox: new entities.ProcessedBoundingBox({
|
||||||
|
x0: w.bbox.x0,
|
||||||
|
y0: w.bbox.y0,
|
||||||
|
x1: w.bbox.x1,
|
||||||
|
y1: w.bbox.y1,
|
||||||
|
}),
|
||||||
|
symbols: w.symbols.map((s: any) => new entities.ProcessedSymbol({
|
||||||
|
fullText: s.text,
|
||||||
|
confidence: s.confidence,
|
||||||
|
boundingBox: new entities.ProcessedBoundingBox({
|
||||||
|
x0: s.bbox.x0,
|
||||||
|
y0: s.bbox.y0,
|
||||||
|
x1: s.bbox.x1,
|
||||||
|
y1: s.bbox.y1,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
return processedArea
|
||||||
|
})
|
||||||
|
|
||||||
|
const addAreaResponses = await Promise.allSettled(addAreaRequests)
|
||||||
|
const areas = addAreaResponses.filter((val): val is PromiseFulfilledResult<entities.ProcessedArea> => val.status === 'fulfilled').map(val => val.value)
|
||||||
|
await saveProcessedText()
|
||||||
|
return areas
|
||||||
|
}
|
||||||
|
|
||||||
|
export default processImageRect
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import { RequestSaveDocumentCollection, RequestSaveGroupCollection, RequestSaveLocalUserProcessedMarkdownCollection, RequestSaveProcessedTextCollection } from '../wailsjs/wailsjs/go/ipc/Channel'
|
import { RequestSaveDocumentCollection, RequestSaveGroupCollection,
|
||||||
|
RequestSaveLocalUserProcessedMarkdownCollection,
|
||||||
|
RequestSaveProcessedTextCollection, RequestSaveContextGroupCollection
|
||||||
|
} from '../wailsjs/wailsjs/go/ipc/Channel'
|
||||||
|
|
||||||
const saveDocuments = async () => {
|
const saveDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
@ -36,9 +39,19 @@ const saveUserProcessedMarkdown = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveContextGroups = async () => {
|
||||||
|
try {
|
||||||
|
const sucessfulSave = await RequestSaveContextGroupCollection()
|
||||||
|
if (!sucessfulSave) console.error('Could not save ContextGroupCollection')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Could not save ContextGroupCollection: ', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
saveDocuments,
|
saveDocuments,
|
||||||
saveGroups,
|
saveGroups,
|
||||||
saveProcessedText,
|
saveProcessedText,
|
||||||
saveUserProcessedMarkdown,
|
saveUserProcessedMarkdown,
|
||||||
|
saveContextGroups,
|
||||||
}
|
}
|
||||||
18
frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts
vendored
18
frontend/wailsjs/wailsjs/go/ipc/Channel.d.ts
vendored
@ -17,10 +17,14 @@ export function GetDocumentById(arg1:string):Promise<entities.Document>;
|
|||||||
|
|
||||||
export function GetDocuments():Promise<ipc.GetDocumentsResponse>;
|
export function GetDocuments():Promise<ipc.GetDocumentsResponse>;
|
||||||
|
|
||||||
|
export function GetProcessedAreaById(arg1:string):Promise<entities.ProcessedArea>;
|
||||||
|
|
||||||
export function GetProcessedAreasByDocumentId(arg1:string):Promise<Array<entities.ProcessedArea>>;
|
export function GetProcessedAreasByDocumentId(arg1:string):Promise<Array<entities.ProcessedArea>>;
|
||||||
|
|
||||||
export function GetProjectByName(arg1:string):Promise<entities.Project>;
|
export function GetProjectByName(arg1:string):Promise<entities.Project>;
|
||||||
|
|
||||||
|
export function GetSerializedContextGroups():Promise<Array<entities.SerializedLinkedProcessedArea>>;
|
||||||
|
|
||||||
export function GetSupportedLanguages():Promise<Array<entities.Language>>;
|
export function GetSupportedLanguages():Promise<Array<entities.Language>>;
|
||||||
|
|
||||||
export function GetUserMarkdownByDocumentId(arg1:string):Promise<entities.UserMarkdown>;
|
export function GetUserMarkdownByDocumentId(arg1:string):Promise<entities.UserMarkdown>;
|
||||||
@ -41,10 +45,18 @@ export function RequestChangeSessionProjectByName(arg1:string):Promise<boolean>;
|
|||||||
|
|
||||||
export function RequestChooseUserAvatar():Promise<string>;
|
export function RequestChooseUserAvatar():Promise<string>;
|
||||||
|
|
||||||
|
export function RequestConnectProcessedAreas(arg1:string,arg2:string):Promise<boolean>;
|
||||||
|
|
||||||
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
|
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
|
||||||
|
|
||||||
export function RequestDeleteDocumentAndChildren(arg1:string):Promise<boolean>;
|
export function RequestDeleteDocumentAndChildren(arg1:string):Promise<boolean>;
|
||||||
|
|
||||||
|
export function RequestDeleteProcessedAreaById(arg1:string):Promise<boolean>;
|
||||||
|
|
||||||
|
export function RequestDisconnectProcessedAreas(arg1:string,arg2:string):Promise<boolean>;
|
||||||
|
|
||||||
|
export function RequestSaveContextGroupCollection():Promise<boolean>;
|
||||||
|
|
||||||
export function RequestSaveDocumentCollection():Promise<boolean>;
|
export function RequestSaveDocumentCollection():Promise<boolean>;
|
||||||
|
|
||||||
export function RequestSaveGroupCollection():Promise<boolean>;
|
export function RequestSaveGroupCollection():Promise<boolean>;
|
||||||
@ -53,7 +65,9 @@ export function RequestSaveLocalUserProcessedMarkdownCollection():Promise<boolea
|
|||||||
|
|
||||||
export function RequestSaveProcessedTextCollection():Promise<boolean>;
|
export function RequestSaveProcessedTextCollection():Promise<boolean>;
|
||||||
|
|
||||||
export function RequestUpdateArea(arg1:entities.Area):Promise<entities.Area>;
|
export function RequestTranslateArea(arg1:string):Promise<boolean>;
|
||||||
|
|
||||||
|
export function RequestUpdateArea(arg1:entities.Area):Promise<boolean>;
|
||||||
|
|
||||||
export function RequestUpdateCurrentUser(arg1:entities.User):Promise<entities.User>;
|
export function RequestUpdateCurrentUser(arg1:entities.User):Promise<entities.User>;
|
||||||
|
|
||||||
@ -61,4 +75,6 @@ export function RequestUpdateDocument(arg1:entities.Document):Promise<entities.D
|
|||||||
|
|
||||||
export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise<entities.UserMarkdown>;
|
export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise<entities.UserMarkdown>;
|
||||||
|
|
||||||
|
export function RequestUpdateProcessedArea(arg1:entities.ProcessedArea):Promise<boolean>;
|
||||||
|
|
||||||
export function RequestUpdateProcessedWordById(arg1:string,arg2:string):Promise<boolean>;
|
export function RequestUpdateProcessedWordById(arg1:string,arg2:string):Promise<boolean>;
|
||||||
|
|||||||
@ -30,6 +30,10 @@ export function GetDocuments() {
|
|||||||
return window['go']['ipc']['Channel']['GetDocuments']();
|
return window['go']['ipc']['Channel']['GetDocuments']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetProcessedAreaById(arg1) {
|
||||||
|
return window['go']['ipc']['Channel']['GetProcessedAreaById'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetProcessedAreasByDocumentId(arg1) {
|
export function GetProcessedAreasByDocumentId(arg1) {
|
||||||
return window['go']['ipc']['Channel']['GetProcessedAreasByDocumentId'](arg1);
|
return window['go']['ipc']['Channel']['GetProcessedAreasByDocumentId'](arg1);
|
||||||
}
|
}
|
||||||
@ -38,6 +42,10 @@ export function GetProjectByName(arg1) {
|
|||||||
return window['go']['ipc']['Channel']['GetProjectByName'](arg1);
|
return window['go']['ipc']['Channel']['GetProjectByName'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetSerializedContextGroups() {
|
||||||
|
return window['go']['ipc']['Channel']['GetSerializedContextGroups']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetSupportedLanguages() {
|
export function GetSupportedLanguages() {
|
||||||
return window['go']['ipc']['Channel']['GetSupportedLanguages']();
|
return window['go']['ipc']['Channel']['GetSupportedLanguages']();
|
||||||
}
|
}
|
||||||
@ -78,6 +86,10 @@ export function RequestChooseUserAvatar() {
|
|||||||
return window['go']['ipc']['Channel']['RequestChooseUserAvatar']();
|
return window['go']['ipc']['Channel']['RequestChooseUserAvatar']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RequestConnectProcessedAreas(arg1, arg2) {
|
||||||
|
return window['go']['ipc']['Channel']['RequestConnectProcessedAreas'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function RequestDeleteAreaById(arg1) {
|
export function RequestDeleteAreaById(arg1) {
|
||||||
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
|
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
|
||||||
}
|
}
|
||||||
@ -86,6 +98,18 @@ export function RequestDeleteDocumentAndChildren(arg1) {
|
|||||||
return window['go']['ipc']['Channel']['RequestDeleteDocumentAndChildren'](arg1);
|
return window['go']['ipc']['Channel']['RequestDeleteDocumentAndChildren'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RequestDeleteProcessedAreaById(arg1) {
|
||||||
|
return window['go']['ipc']['Channel']['RequestDeleteProcessedAreaById'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestDisconnectProcessedAreas(arg1, arg2) {
|
||||||
|
return window['go']['ipc']['Channel']['RequestDisconnectProcessedAreas'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestSaveContextGroupCollection() {
|
||||||
|
return window['go']['ipc']['Channel']['RequestSaveContextGroupCollection']();
|
||||||
|
}
|
||||||
|
|
||||||
export function RequestSaveDocumentCollection() {
|
export function RequestSaveDocumentCollection() {
|
||||||
return window['go']['ipc']['Channel']['RequestSaveDocumentCollection']();
|
return window['go']['ipc']['Channel']['RequestSaveDocumentCollection']();
|
||||||
}
|
}
|
||||||
@ -102,6 +126,10 @@ export function RequestSaveProcessedTextCollection() {
|
|||||||
return window['go']['ipc']['Channel']['RequestSaveProcessedTextCollection']();
|
return window['go']['ipc']['Channel']['RequestSaveProcessedTextCollection']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RequestTranslateArea(arg1) {
|
||||||
|
return window['go']['ipc']['Channel']['RequestTranslateArea'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function RequestUpdateArea(arg1) {
|
export function RequestUpdateArea(arg1) {
|
||||||
return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1);
|
return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1);
|
||||||
}
|
}
|
||||||
@ -118,6 +146,10 @@ export function RequestUpdateDocumentUserMarkdown(arg1, arg2) {
|
|||||||
return window['go']['ipc']['Channel']['RequestUpdateDocumentUserMarkdown'](arg1, arg2);
|
return window['go']['ipc']['Channel']['RequestUpdateDocumentUserMarkdown'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RequestUpdateProcessedArea(arg1) {
|
||||||
|
return window['go']['ipc']['Channel']['RequestUpdateProcessedArea'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function RequestUpdateProcessedWordById(arg1, arg2) {
|
export function RequestUpdateProcessedWordById(arg1, arg2) {
|
||||||
return window['go']['ipc']['Channel']['RequestUpdateProcessedWordById'](arg1, arg2);
|
return window['go']['ipc']['Channel']['RequestUpdateProcessedWordById'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export namespace entities {
|
|||||||
endX: number;
|
endX: number;
|
||||||
endY: number;
|
endY: number;
|
||||||
language: Language;
|
language: Language;
|
||||||
|
translateLanguage: Language;
|
||||||
order: number;
|
order: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@ -41,6 +42,7 @@ export namespace entities {
|
|||||||
this.endX = source["endX"];
|
this.endX = source["endX"];
|
||||||
this.endY = source["endY"];
|
this.endY = source["endY"];
|
||||||
this.language = this.convertValues(source["language"], Language);
|
this.language = this.convertValues(source["language"], Language);
|
||||||
|
this.translateLanguage = this.convertValues(source["translateLanguage"], Language);
|
||||||
this.order = source["order"];
|
this.order = source["order"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +241,7 @@ export namespace entities {
|
|||||||
}
|
}
|
||||||
export class ProcessedWord {
|
export class ProcessedWord {
|
||||||
id: string;
|
id: string;
|
||||||
|
areaId: string;
|
||||||
fullText: string;
|
fullText: string;
|
||||||
symbols: ProcessedSymbol[];
|
symbols: ProcessedSymbol[];
|
||||||
confidence: number;
|
confidence: number;
|
||||||
@ -252,6 +255,7 @@ export namespace entities {
|
|||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
|
this.areaId = source["areaId"];
|
||||||
this.fullText = source["fullText"];
|
this.fullText = source["fullText"];
|
||||||
this.symbols = this.convertValues(source["symbols"], ProcessedSymbol);
|
this.symbols = this.convertValues(source["symbols"], ProcessedSymbol);
|
||||||
this.confidence = source["confidence"];
|
this.confidence = source["confidence"];
|
||||||
@ -418,6 +422,22 @@ export namespace entities {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SerializedLinkedProcessedArea {
|
||||||
|
areaId: string;
|
||||||
|
previousId: string;
|
||||||
|
nextId: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new SerializedLinkedProcessedArea(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.areaId = source["areaId"];
|
||||||
|
this.previousId = source["previousId"];
|
||||||
|
this.nextId = source["nextId"];
|
||||||
|
}
|
||||||
|
}
|
||||||
export class Session {
|
export class Session {
|
||||||
project: Project;
|
project: Project;
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
@ -477,6 +497,7 @@ export namespace ipc {
|
|||||||
export class GetDocumentsResponse {
|
export class GetDocumentsResponse {
|
||||||
documents: entities.Document[];
|
documents: entities.Document[];
|
||||||
groups: entities.Group[];
|
groups: entities.Group[];
|
||||||
|
contextGroups: entities.SerializedLinkedProcessedArea[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new GetDocumentsResponse(source);
|
return new GetDocumentsResponse(source);
|
||||||
@ -486,6 +507,7 @@ export namespace ipc {
|
|||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.documents = this.convertValues(source["documents"], entities.Document);
|
this.documents = this.convertValues(source["documents"], entities.Document);
|
||||||
this.groups = this.convertValues(source["groups"], entities.Group);
|
this.groups = this.convertValues(source["groups"], entities.Group);
|
||||||
|
this.contextGroups = this.convertValues(source["contextGroups"], entities.SerializedLinkedProcessedArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ go 1.18
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/snakesel/libretranslate v0.0.2
|
||||||
github.com/wailsapp/wails/v2 v2.5.1
|
github.com/wailsapp/wails/v2 v2.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -38,6 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw=
|
github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw=
|
||||||
github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
|
github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
|
||||||
|
github.com/snakesel/libretranslate v0.0.2 h1:6LG/UMMpGtoj3NXvlzsxZgQEH0Qsi62jCDd5Yq5ALL8=
|
||||||
|
github.com/snakesel/libretranslate v0.0.2/go.mod h1:B8F8Dda8RlkHRMzs/aw8DWj9HfyHSXpaJTFD391hEUI=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package ipc
|
package ipc
|
||||||
|
|
||||||
|
import (
|
||||||
|
document "textualize/core/Document"
|
||||||
|
"textualize/translate"
|
||||||
|
)
|
||||||
|
|
||||||
type Channel struct{}
|
type Channel struct{}
|
||||||
|
|
||||||
var channelInstance *Channel
|
var channelInstance *Channel
|
||||||
@ -11,3 +16,34 @@ func GetInstance() *Channel {
|
|||||||
|
|
||||||
return channelInstance
|
return channelInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Channel) RequestTranslateArea(areaId string) bool {
|
||||||
|
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(areaId)
|
||||||
|
area := documentOfArea.GetAreaById(areaId)
|
||||||
|
processedArea := document.GetProcessedAreaCollection().GetAreaById(area.Id)
|
||||||
|
|
||||||
|
var textToTranslate string
|
||||||
|
for _, line := range processedArea.Lines {
|
||||||
|
for _, word := range line.Words {
|
||||||
|
textToTranslate = textToTranslate + " " + word.FullText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceLanguage string
|
||||||
|
if area.Language.TranslateCode != "" {
|
||||||
|
sourceLanguage = area.Language.TranslateCode
|
||||||
|
} else if documentOfArea.DefaultLanguage.TranslateCode != "" {
|
||||||
|
sourceLanguage = documentOfArea.DefaultLanguage.TranslateCode
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceLanguage = "he"
|
||||||
|
targetLanguage := "en"
|
||||||
|
translatedText := translate.Text(textToTranslate, sourceLanguage, targetLanguage)
|
||||||
|
if translatedText == "" {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
68
ipc/ContextGroup.go
Normal file
68
ipc/ContextGroup.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package ipc
|
||||||
|
|
||||||
|
import (
|
||||||
|
contextGroup "textualize/core/ContextGroup"
|
||||||
|
document "textualize/core/Document"
|
||||||
|
"textualize/entities"
|
||||||
|
"textualize/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Channel) RequestDisconnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
|
||||||
|
contextGroupCollection := contextGroup.GetContextGroupCollection()
|
||||||
|
|
||||||
|
wasSuccessfulDisconnect := contextGroupCollection.DisconnectProcessedAreas(ancestorAreaId, descendantAreaId)
|
||||||
|
if wasSuccessfulDisconnect {
|
||||||
|
wasSuccessfulWrite := c.RequestSaveContextGroupCollection()
|
||||||
|
return wasSuccessfulWrite
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
If a connection already exists, then this method will default to disconnecting the two areas.
|
||||||
|
*/
|
||||||
|
func (c *Channel) RequestConnectProcessedAreas(ancestorAreaId string, descendantAreaId string) bool {
|
||||||
|
contextGroupCollection := contextGroup.GetContextGroupCollection()
|
||||||
|
|
||||||
|
doesContextGroupAlreadyExist := contextGroupCollection.DoesGroupExistBetweenProcessedAreas(ancestorAreaId, descendantAreaId)
|
||||||
|
if doesContextGroupAlreadyExist {
|
||||||
|
return c.RequestDisconnectProcessedAreas(ancestorAreaId, descendantAreaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
processedAreaCollection := document.GetProcessedAreaCollection()
|
||||||
|
|
||||||
|
ancestorArea := processedAreaCollection.GetAreaById(ancestorAreaId)
|
||||||
|
descendantArea := processedAreaCollection.GetAreaById(descendantAreaId)
|
||||||
|
|
||||||
|
wasSuccessfulConnect := contextGroupCollection.ConnectProcessedAreas(*ancestorArea, *descendantArea)
|
||||||
|
if wasSuccessfulConnect {
|
||||||
|
wasSuccessfulWrite := c.RequestSaveContextGroupCollection()
|
||||||
|
return wasSuccessfulWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) GetSerializedContextGroups() []entities.SerializedLinkedProcessedArea {
|
||||||
|
contextGroupCollection := contextGroup.GetContextGroupCollection()
|
||||||
|
|
||||||
|
serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0)
|
||||||
|
for _, group := range contextGroupCollection.Groups {
|
||||||
|
serializedContextGroups = append(serializedContextGroups, group.Serialize()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializedContextGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) RequestSaveContextGroupCollection() bool {
|
||||||
|
contextGroupCollection := contextGroup.GetContextGroupCollection()
|
||||||
|
projectName := c.GetCurrentSession().Project.Name
|
||||||
|
|
||||||
|
serializedContextGroups := make([]entities.SerializedLinkedProcessedArea, 0)
|
||||||
|
for _, group := range contextGroupCollection.Groups {
|
||||||
|
serializedContextGroups = append(serializedContextGroups, group.Serialize()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulWrite := storage.GetDriver().WriteContextGroupCollection(serializedContextGroups, projectName)
|
||||||
|
return successfulWrite
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package ipc
|
package ipc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
app "textualize/core/App"
|
app "textualize/core/App"
|
||||||
document "textualize/core/Document"
|
document "textualize/core/Document"
|
||||||
@ -15,6 +16,7 @@ import (
|
|||||||
type GetDocumentsResponse struct {
|
type GetDocumentsResponse struct {
|
||||||
Documents []entities.Document `json:"documents"`
|
Documents []entities.Document `json:"documents"`
|
||||||
Groups []entities.Group `json:"groups"`
|
Groups []entities.Group `json:"groups"`
|
||||||
|
ContextGroups []entities.SerializedLinkedProcessedArea `json:"contextGroups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Channel) GetDocumentById(id string) entities.Document {
|
func (c *Channel) GetDocumentById(id string) entities.Document {
|
||||||
@ -25,10 +27,12 @@ func (c *Channel) GetDocumentById(id string) entities.Document {
|
|||||||
func (c *Channel) GetDocuments() GetDocumentsResponse {
|
func (c *Channel) GetDocuments() GetDocumentsResponse {
|
||||||
documents := document.GetDocumentCollection().Documents
|
documents := document.GetDocumentCollection().Documents
|
||||||
groups := document.GetGroupCollection().Groups
|
groups := document.GetGroupCollection().Groups
|
||||||
|
contextGroups := c.GetSerializedContextGroups()
|
||||||
|
|
||||||
response := GetDocumentsResponse{
|
response := GetDocumentsResponse{
|
||||||
Groups: make([]entities.Group, 0),
|
Groups: make([]entities.Group, 0),
|
||||||
Documents: make([]entities.Document, 0),
|
Documents: make([]entities.Document, 0),
|
||||||
|
ContextGroups: contextGroups,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range documents {
|
for _, d := range documents {
|
||||||
@ -221,17 +225,17 @@ func (c *Channel) RequestAddArea(documentId string, area entities.Area) entities
|
|||||||
return newArea
|
return newArea
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Channel) RequestUpdateArea(updatedArea entities.Area) entities.Area {
|
func (c *Channel) RequestUpdateArea(updatedArea entities.Area) bool {
|
||||||
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(updatedArea.Id)
|
documentOfArea := document.GetDocumentCollection().GetDocumentByAreaId(updatedArea.Id)
|
||||||
|
|
||||||
if documentOfArea.Id == "" {
|
if documentOfArea.Id == "" {
|
||||||
return entities.Area{}
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
areaToUpdate := documentOfArea.GetAreaById(updatedArea.Id)
|
areaToUpdate := documentOfArea.GetAreaById(updatedArea.Id)
|
||||||
|
|
||||||
if areaToUpdate.Id == "" {
|
if areaToUpdate.Id == "" {
|
||||||
return entities.Area{}
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if updatedArea.Name != "" {
|
if updatedArea.Name != "" {
|
||||||
@ -240,8 +244,14 @@ func (c *Channel) RequestUpdateArea(updatedArea entities.Area) entities.Area {
|
|||||||
if updatedArea.Order != areaToUpdate.Order {
|
if updatedArea.Order != areaToUpdate.Order {
|
||||||
areaToUpdate.Order = updatedArea.Order
|
areaToUpdate.Order = updatedArea.Order
|
||||||
}
|
}
|
||||||
|
if updatedArea.Language.ProcessCode != "" {
|
||||||
|
areaToUpdate.Language = updatedArea.Language
|
||||||
|
}
|
||||||
|
|
||||||
return *areaToUpdate
|
fmt.Println(areaToUpdate.Language)
|
||||||
|
fmt.Println(documentOfArea.GetAreaById(updatedArea.Id))
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Channel) RequestDeleteAreaById(areaId string) bool {
|
func (c *Channel) RequestDeleteAreaById(areaId string) bool {
|
||||||
|
|||||||
@ -8,6 +8,15 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (c *Channel) GetProcessedAreaById(id string) entities.ProcessedArea {
|
||||||
|
foundArea := document.GetProcessedAreaCollection().GetAreaById(id)
|
||||||
|
if foundArea != nil {
|
||||||
|
return *foundArea
|
||||||
|
} else {
|
||||||
|
return entities.ProcessedArea{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Channel) GetProcessedAreasByDocumentId(id string) []entities.ProcessedArea {
|
func (c *Channel) GetProcessedAreasByDocumentId(id string) []entities.ProcessedArea {
|
||||||
areas := document.GetProcessedAreaCollection().GetAreasByDocumentId(id)
|
areas := document.GetProcessedAreaCollection().GetAreasByDocumentId(id)
|
||||||
|
|
||||||
@ -36,7 +45,51 @@ func (c *Channel) RequestAddProcessedArea(processedArea entities.ProcessedArea)
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.GetProcessedAreaCollection().AddProcessedArea(processedArea)
|
document.GetProcessedAreaCollection().AddProcessedArea(processedArea)
|
||||||
return processedArea
|
return *document.GetProcessedAreaCollection().GetAreaById(processedArea.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) RequestDeleteProcessedAreaById(id string) bool {
|
||||||
|
processedAreas := document.GetProcessedAreaCollection().Areas
|
||||||
|
areaToUpdate := document.GetProcessedAreaCollection().GetAreaById(id)
|
||||||
|
if areaToUpdate.Id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
areaToDeleteIndex := -1
|
||||||
|
|
||||||
|
for i, a := range processedAreas {
|
||||||
|
if a.Id == id {
|
||||||
|
areaToDeleteIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if areaToDeleteIndex < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
processedAreas[areaToDeleteIndex] = processedAreas[len(processedAreas)-1]
|
||||||
|
// processedAreas = processedAreas[:len(processedAreas)-1]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) RequestUpdateProcessedArea(updatedProcessedArea entities.ProcessedArea) bool {
|
||||||
|
if updatedProcessedArea.Id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
successfulDelete := c.RequestDeleteProcessedAreaById(updatedProcessedArea.Id)
|
||||||
|
if !successfulDelete {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addedProcessedArea := c.RequestAddProcessedArea(updatedProcessedArea)
|
||||||
|
return addedProcessedArea.Id != ""
|
||||||
|
|
||||||
|
// if addedProcessedArea.Id != "" {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Channel) RequestUpdateProcessedWordById(wordId string, newTextValue string) bool {
|
func (c *Channel) RequestUpdateProcessedWordById(wordId string, newTextValue string) bool {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package ipc
|
|||||||
import (
|
import (
|
||||||
app "textualize/core/App"
|
app "textualize/core/App"
|
||||||
consts "textualize/core/Consts"
|
consts "textualize/core/Consts"
|
||||||
|
contextGroup "textualize/core/ContextGroup"
|
||||||
document "textualize/core/Document"
|
document "textualize/core/Document"
|
||||||
session "textualize/core/Session"
|
session "textualize/core/Session"
|
||||||
"textualize/entities"
|
"textualize/entities"
|
||||||
@ -144,6 +145,7 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
|
|||||||
|
|
||||||
session.GetInstance().Project = foundProject
|
session.GetInstance().Project = foundProject
|
||||||
|
|
||||||
|
// Documents
|
||||||
localDocumentCollection := storageDriver.ReadDocumentCollection(projectName)
|
localDocumentCollection := storageDriver.ReadDocumentCollection(projectName)
|
||||||
documentCount := len(localDocumentCollection.Documents)
|
documentCount := len(localDocumentCollection.Documents)
|
||||||
readableDocuments := make([]document.Entity, documentCount)
|
readableDocuments := make([]document.Entity, documentCount)
|
||||||
@ -155,6 +157,7 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
|
|||||||
ProjectId: foundProject.Id,
|
ProjectId: foundProject.Id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Groups
|
||||||
localGroupsCollection := storageDriver.ReadGroupCollection(projectName)
|
localGroupsCollection := storageDriver.ReadGroupCollection(projectName)
|
||||||
groupCount := len(localGroupsCollection.Groups)
|
groupCount := len(localGroupsCollection.Groups)
|
||||||
readableGroups := make([]entities.Group, groupCount)
|
readableGroups := make([]entities.Group, groupCount)
|
||||||
@ -167,6 +170,10 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
|
|||||||
Groups: readableGroups,
|
Groups: readableGroups,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Context Groups
|
||||||
|
localSerializedContextGroups := storageDriver.ReadContextGroupCollection(projectName)
|
||||||
|
contextGroup.SetContextGroupCollectionBySerialized(localSerializedContextGroups)
|
||||||
|
|
||||||
// Processed Texts
|
// Processed Texts
|
||||||
localProcessedAreaCollection := storageDriver.ReadProcessedTextCollection(projectName)
|
localProcessedAreaCollection := storageDriver.ReadProcessedTextCollection(projectName)
|
||||||
areaCount := len(localProcessedAreaCollection.Areas)
|
areaCount := len(localProcessedAreaCollection.Areas)
|
||||||
|
|||||||
22
storage/Local/ContextGroupDriver.go
Normal file
22
storage/Local/ContextGroupDriver.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"textualize/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d LocalDriver) WriteContextGroupCollection(serializedContextGroups []entities.SerializedLinkedProcessedArea, projectName string) bool {
|
||||||
|
jsonData, _ := json.MarshalIndent(serializedContextGroups, "", " ")
|
||||||
|
writeError := WriteDataToAppDir(jsonData, "/projects/"+projectName+"/", "ContextGroups.json")
|
||||||
|
return writeError == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d LocalDriver) ReadContextGroupCollection(projectName string) []entities.SerializedLinkedProcessedArea {
|
||||||
|
contextGroupCollectionData := make([]entities.SerializedLinkedProcessedArea, 0)
|
||||||
|
readError := AssignFileDataToStruct("/projects/"+projectName+"/ContextGroups.json", &contextGroupCollectionData)
|
||||||
|
if readError != nil {
|
||||||
|
return make([]entities.SerializedLinkedProcessedArea, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextGroupCollectionData
|
||||||
|
}
|
||||||
@ -19,6 +19,8 @@ type Driver interface {
|
|||||||
ReadProcessedTextCollection(string) entities.ProcessedTextCollection
|
ReadProcessedTextCollection(string) entities.ProcessedTextCollection
|
||||||
WriteProcessedUserMarkdownCollection(entities.ProcessedUserMarkdownCollection, string) bool
|
WriteProcessedUserMarkdownCollection(entities.ProcessedUserMarkdownCollection, string) bool
|
||||||
ReadProcessedUserMarkdownCollection(string) entities.ProcessedUserMarkdownCollection
|
ReadProcessedUserMarkdownCollection(string) entities.ProcessedUserMarkdownCollection
|
||||||
|
WriteContextGroupCollection([]entities.SerializedLinkedProcessedArea, string) bool
|
||||||
|
ReadContextGroupCollection(string) []entities.SerializedLinkedProcessedArea
|
||||||
}
|
}
|
||||||
|
|
||||||
var driverInstance Driver
|
var driverInstance Driver
|
||||||
|
|||||||
29
translate/Translate.go
Normal file
29
translate/Translate.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package translate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/snakesel/libretranslate"
|
||||||
|
// tr "github.com/snakesel/libretranslate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var translatorInstance *libretranslate.Translation
|
||||||
|
|
||||||
|
func GetTranslator() *libretranslate.Translation {
|
||||||
|
return libretranslate.New(libretranslate.Config{
|
||||||
|
Url: "http://localhost:9090",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Text(value string, sourceLanguage string, targetLanguage string) string {
|
||||||
|
translator := GetTranslator()
|
||||||
|
|
||||||
|
responseText, err := translator.Translate(value, sourceLanguage, targetLanguage)
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println(responseText)
|
||||||
|
return responseText
|
||||||
|
} else {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
return ("")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user