Compare commits

...

7 Commits

Author SHA1 Message Date
Yehoshua Sandler
806f4a28e4
Refactor Stage Context to redux (#7)
* feat: setup stage slice

* refact: replace useStage() with redux
2023-09-04 11:32:54 -05:00
Yehoshua Sandler
095c1ca8ec
Refactor notifications to redux (#6)
* feat: make new connections

refact: context groups | feat: area detection

and a bunch of small things. hate yourself for this massive commit

* refact: initial RTK setup for Notifications

* refact: removed Notification Context
2023-09-04 10:02:08 -05:00
Yehoshua Sandler
7dd6de064f
Refactor Context Groups & Area Detection (#4)
* feat: make new connections

* refact: context groups | feat: area detection

and a bunch of small things. hate yourself for this massive commit
2023-09-02 10:58:38 -05:00
Joshua Shoemaker
ee5ac6ea69 basic init readme 2023-07-24 08:01:25 -05:00
Joshua Shoemaker
f129a9cb13 feat: starting features of translation & contexts 2023-07-24 07:45:03 -05:00
Yehoshua Sandler
917662e9ba
feat: context menu & notification system (#3) 2023-07-01 12:31:25 -05:00
Yehoshua Sandler
1631271b93
Replace native canvas implementation with Konva library (#2)
* style: spelling

* refact: canvases replaced with konva

* refact: area text calculated by words

* refact: moved konva files out of test dir
2023-06-27 08:42:44 -05:00
80 changed files with 2990 additions and 863 deletions

View File

@ -3,7 +3,15 @@
"*.css": "tailwindcss" "*.css": "tailwindcss"
}, },
"cSpell.words": [ "cSpell.words": [
"consts",
"headlessui",
"heroicons", "heroicons",
"konva",
"libretranslate",
"reduxjs",
"tailwindcss",
"Tesseract",
"Textualize",
"wailsjs" "wailsjs"
] ]
} }

View File

@ -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.
![image](/docs/assets/overviewScreenshot.png)
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

View File

@ -2,17 +2,25 @@ package consts
import "textualize/entities" import "textualize/entities"
func GetSuppportedLanguages() []entities.Language { func GetSupportedLanguages() []entities.Language {
return []entities.Language{ return []entities.Language{
{ {
DisplayName: "English", DisplayName: "English",
ProcessCode: "eng", ProcessCode: "eng",
TranslateCode: "en", TranslateCode: "en",
IsBundledCustom: false,
}, },
{ {
DisplayName: "Hebrew", DisplayName: "Hebrew - Classic",
ProcessCode: "heb", ProcessCode: "heb",
TranslateCode: "he", TranslateCode: "he",
IsBundledCustom: false,
},
{
DisplayName: "Hebrew - Rashi",
ProcessCode: "heb_rashi",
TranslateCode: "he",
IsBundledCustom: true,
}, },
} }
} }

View 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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

173
entities/ContextGroup.go Normal file
View 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
}

View File

@ -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"`
} }

View File

@ -4,4 +4,5 @@ type Language struct {
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
ProcessCode string `json:"processCode"` ProcessCode string `json:"processCode"`
TranslateCode string `json:"translateCode"` TranslateCode string `json:"translateCode"`
IsBundledCustom bool `json:"isBundledCustom"`
} }

View File

@ -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"`
@ -23,14 +24,12 @@ type ProcessedWord struct {
} }
type ProcessedLine struct { type ProcessedLine struct {
FullText string `json:"fullText"`
Words []ProcessedWord `json:"words"` Words []ProcessedWord `json:"words"`
} }
type ProcessedArea struct { type ProcessedArea struct {
Id string `json:"id"` Id string `json:"id"`
DocumentId string `json:"documentId"` DocumentId string `json:"documentId"`
FullText string `json:"fullText"`
Order int `json:"order"` Order int `json:"order"`
Lines []ProcessedLine `json:"lines"` Lines []ProcessedLine `json:"lines"`
} }

View File

@ -0,0 +1,79 @@
'use client'
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import Konva from 'konva'
import { Group, Rect } from 'react-konva'
import { KonvaEventObject } from 'konva/lib/Node'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { useProject } from '../../context/Project/provider'
import AreaContextMenu from './AreaContextMenu'
import { RootState } from '../../redux/store'
type Props = {
isActive: boolean,
area: entities.Area,
}
type coordinates = { x: number, y: number }
const Area = (props: Props) => {
const { scale } = useSelector((state: RootState) => state.stage)
const { selectedAreaId, setSelectedAreaId } = useProject()
const shapeRef = React.useRef<Konva.Rect>(null)
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()
const { area, isActive } = props
const a = area
const width = (a.endX - a.startX)
const height = (a.endY - a.startY)
const handleContextMenu = (e: KonvaEventObject<PointerEvent>) => {
e.evt.preventDefault()
const stage = e.currentTarget.getStage()
const pointerPosition = stage?.getRelativePointerPosition()
if (!pointerPosition) return
const x = pointerPosition.x + 4
const y = pointerPosition.y + 4
setAreaContextMenuPosition({ x, y })
setIsAreaContextMenuOpen(true)
}
const handleAreaClick = (areaId: string) => {
if (areaId === selectedAreaId) setSelectedAreaId('')
else setSelectedAreaId(areaId)
}
return <Group>
<Rect
ref={shapeRef}
id={a.id}
width={width}
height={height}
x={a.startX * scale}
y={a.startY * scale}
scale={{ x: scale, y: scale }}
strokeEnabled
stroke={isActive ? '#dc8dec' : '#1e1e1e'}
strokeWidth={1}
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
onClick={() => handleAreaClick(a.id)}
onContextMenu={handleContextMenu}
isArea
/>
{isAreaContextMenuOpen
? <AreaContextMenu
area={area}
x={areaContextMenuPosition?.x || 0}
y={areaContextMenuPosition?.y || 0}
scale={scale}
setIsAreaContextMenuOpen={setIsAreaContextMenuOpen} />
: <></>
}
</Group>
}
export default Area

View File

@ -1,69 +0,0 @@
'use client'
import React, { useEffect, useRef } from 'react'
import { useProject } from '../../context/Project/provider'
type Props = {
width: number,
height: number
zoomLevel: number
}
const AreaCanvas = (props: Props) => {
const { getSelectedDocument, selectedAreaId, } = useProject()
const canvas = useRef<HTMLCanvasElement>(null)
const areas = getSelectedDocument()?.areas
const { width, height, zoomLevel } = props
const applyAreasToCanvas = (zoomLevel: number) => {
if (!areas || !areas.length) return
const canvasContext = canvas.current!.getContext('2d')!
areas.forEach(a => {
canvasContext.beginPath()
if (a.id !== selectedAreaId) {
canvasContext.setLineDash([4])
canvasContext.lineWidth = 2
canvasContext.strokeStyle = '#010101'
} else {
canvasContext.setLineDash([])
canvasContext.lineWidth = 3
canvasContext.strokeStyle = '#dc8dec'
}
const width = (a.endX - a.startX) * zoomLevel
const height = (a.endY - a.startY) * zoomLevel
const x = a.startX * zoomLevel
const y = a.startY * zoomLevel
canvasContext.roundRect(x, y, width, height, 4)
canvasContext.stroke()
canvasContext.closePath()
})
}
const clearCanvas = () => {
const canvasInstance = canvas.current!
const context = canvasInstance.getContext('2d')!
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
}
const updateSize = (size: { width: number, height: number }) => {
const canvasInstance = canvas.current!
const { width, height } = size
canvasInstance.width = width
canvasInstance.height = height
}
useEffect(() => {
clearCanvas()
updateSize({ width, height })
applyAreasToCanvas(zoomLevel)
}, [width, height, zoomLevel, areas])
return <canvas className="absolute" ref={canvas} />
}
export default AreaCanvas

View File

@ -0,0 +1,195 @@
'use client'
import React, { useState } from 'react'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { Html } from 'react-konva-utils'
import { ClipboardIcon, ArrowPathIcon, TrashIcon, LanguageIcon } from '@heroicons/react/24/outline'
import { getScaled, makeFormStyles, makeIconStyles } from './styles'
import { useProject } from '../../../context/Project/provider'
import asyncClick from '../../../utils/asyncClick'
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 = {
x: number,
y: number,
scale: number,
area: entities.Area,
setIsAreaContextMenuOpen: Function
}
const AreaContextMenu = (props: Props) => {
const dispatch = useDispatch()
const { getProcessedAreaById, requestDeleteAreaById, getSelectedDocument, requestUpdateArea } = useProject()
const [shouldShowProcessLanguageSelect, setShouldShowProcessLanguageSelect] = useState(false)
const { area, setIsAreaContextMenuOpen, scale, x, y } = props
const handleCopyButtonClick = async () => {
setIsAreaContextMenuOpen(false)
const processedArea = await getProcessedAreaById(area.id)
const wordsOfProcessedArea = processedArea?.lines.flatMap(l => l.words.map(w => w.fullText))
const fullText = wordsOfProcessedArea?.join(' ')
if (!fullText) {
dispatch(pushNotification({ message: 'No text found to copy.', level: 'warning' }))
return
}
try {
await navigator.clipboard.writeText(fullText)
dispatch(pushNotification({ message: 'Copied area to clipboard' }))
} catch (err) {
dispatch(pushNotification({ message: 'Error copying area', level: 'error' }))
}
}
const handleDeleteButtonClick = async () => {
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 () => {
setIsAreaContextMenuOpen(false)
const documentId = getSelectedDocument()?.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>
<div style={makeFormStyles(x, y, scale)} tabIndex={1} onBlur={handleOnBlur}>
<div className={classNames(
'z-40 min-w-max py-1 rounded-lg shadow-sm outline-none font-light',
'bg-white border border-gray-200',)}
>
<button autoFocus tabIndex={2}
onClick={(e) => asyncClick(e, handleCopyButtonClick)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">Copy Area</span>
<ClipboardIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
<button tabIndex={3}
onClick={(e) => asyncClick(e, handleReprocessButtonClick)} className={
classNames(baseMenuItemClassNames,
'focus:bg-neutral-100 hover:bg-slate-300',
)}>
<span className="mr-2">Reprocess Area</span>
<ArrowPathIcon className="ml-2" aria-hidden="true" style={{ ...makeIconStyles(scale) }} />
</button>
<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 >
}
export default AreaContextMenu

View File

@ -0,0 +1,31 @@
import { DetailedHTMLProps, FormHTMLAttributes } from 'react'
const getScaled = (value: number, scale: number) => Math.floor(value / scale)
const makeFormStyles = (x: number, y: number, scale: number) => {
const shadowOffset = { x: getScaled(4, scale), y: getScaled(4, scale), color: 'rgba(50, 50, 50, 0.4)', blur: getScaled(20, scale) }
return {
position: 'absolute',
fontSize: `${getScaled(16, scale)}px`,
width: `${getScaled(224, scale)}px`,
left: `${x}px`,
top: `${y}px`,
boxShadow: `${shadowOffset.x}px ${shadowOffset.y}px ${shadowOffset.blur}px ${shadowOffset.color}`
} as DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
}
const makeIconStyles = (scale: number) => {
return {
width: `${getScaled(14, scale)}px`,
height: `${getScaled(14, scale)}px`
}
}
export {
makeFormStyles,
makeIconStyles,
getScaled,
}

View File

@ -1,42 +0,0 @@
import React from 'react'
import { entities } from '../../wailsjs/wailsjs/go/models'
import classNames from '../../utils/classNames'
type Props = {
areas: entities.Area[]
processedArea?: entities.ProcessedArea
zoomLevel: number
setWordToEdit: (props: { word: entities.ProcessedWord, areaId: string }) => void
}
const AreaTextPreview = ({ areas, processedArea, zoomLevel, setWordToEdit }: Props) => {
if (!processedArea) return <></>
return <div>
{
processedArea.lines?.map(l => l.words).flat().map((w, i) => {
const width = Math.floor((w.boundingBox.x1 - w.boundingBox.x0) * zoomLevel) + 2
const height = Math.floor((w.boundingBox.y1 - w.boundingBox.y0) * zoomLevel) + 2
return <span
key={i}
dir={w.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
className={classNames('absolute text-center inline-block p-1 rounded-md shadow-zinc-900 shadow-2xl',
'hover:bg-opacity-60 hover:bg-black hover:text-white',
'bg-opacity-80 bg-slate-300 text-slate-500'
)}
style={{
fontSize: `${3.4 * zoomLevel}vmin`,
width,
top: Math.floor(w.boundingBox.y0 * zoomLevel) + height,
left: Math.floor(w.boundingBox.x0 * zoomLevel)
}}
onDoubleClick={() => setWordToEdit({ word: w, areaId: processedArea.id })}>
{w.fullText}
</span>
})
}
</div>
}
export default AreaTextPreview

View File

@ -0,0 +1,71 @@
'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 { entities } from '../../wailsjs/wailsjs/go/models'
import Area from './Area'
import ProcessedWord from './ProcessedWord'
import EditingWord from './EditingWord'
import { RootState } from '../../redux/store'
type Props = { scale: number }
const Areas = ({ scale }: Props) => {
const { areProcessedWordsVisible } = useSelector((state: RootState) => state.stage)
const { getSelectedDocument, selectedAreaId, getProcessedAreaById } = useProject()
const areas = getSelectedDocument()?.areas || []
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 = () => {
if (!editingWord) return
return <EditingWord
scale={scale}
editingWord={editingWord}
setEditingWord={setEditingWord}
/>
}
const renderProcessedWords = () => {
if (!selectedProcessedArea) return <></>
const words = selectedProcessedArea.lines.map(l => l.words).flat()
return words.map((w, index) => <ProcessedWord
key={index}
area={selectedProcessedArea}
word={w}
scale={scale}
setEditingWord={setEditingWord}
/>)
}
const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => {
return <Area key={index} area={a} isActive={a.id === selectedAreaId} />
})
return <Group>
{renderAreas(areas)}
{areProcessedWordsVisible ? renderProcessedWords() : <></>}
{areProcessedWordsVisible ? renderEditingWord() : <></>}
</Group>
}
export default Areas

View File

@ -0,0 +1,118 @@
'use client'
import React, { useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Stage, Layer, Image, } from 'react-konva'
import { KonvaEventObject } from 'konva/lib/Node'
import Areas from './Areas'
import { useProject } from '../../context/Project/provider'
import useImage from 'use-image'
import { RectangleCoordinates } from './types'
import DrawingArea from './DrawingArea'
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
import ContextConnections from './ContextConnections'
import processImageRect from '../../useCases/processImageRect'
import { RootState } from '../../redux/store'
import { maxScale, scaleStep, setIsDrawingArea, setScale, setStartingContextConnectionPoint } from '../../redux/features/stage/stageSlice'
let downClickX: number
let downClickY: number
const CanvasStage = () => {
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 documentRef = useRef(null)
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
const documentWidth = documentImage?.naturalWidth || 0
const documentHeight = documentImage?.naturalHeight || 0
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()
const position = e.currentTarget.getRelativePointerPosition()
downClickX = position.x
downClickY = position.y
dispatch(setIsDrawingArea(true))
}
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
const currentPosition = e.currentTarget.getRelativePointerPosition()
if (isDrawingArea) return setDrawingAreaRect({
startX: downClickX,
startY: downClickY,
endX: currentPosition.x,
endY: currentPosition.y,
})
}
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.currentTarget
if (stage.isDragging()) stage.stopDrag()
else if (isDrawingArea) dispatch(setIsDrawingArea(false))
if (!drawingAreaRect) return
const normalizedDrawnRect = getNormalizedRectToBounds(drawingAreaRect, documentWidth, documentHeight, scale)
const selectedDocumentId = getSelectedDocument()!.id
processImageRect(selectedDocumentId, normalizedDrawnRect).then(async addedAreas => {
updateDocuments().then(response => {
if (!addedAreas.length) return
setSelectedAreaId(addedAreas[0].id)
})
})
setDrawingAreaRect(null)
}
const handleWheel = (e: KonvaEventObject<WheelEvent>) => {
if (!e.evt.ctrlKey) return
const wheelDelta = e.evt.deltaY
const shouldAttemptScaleUp = (wheelDelta < 0) && scale < maxScale
if (shouldAttemptScaleUp) dispatch(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}>
<Layer id='documentLayer'>
<Image alt='Document Image'
ref={documentRef}
image={documentImage}
width={documentWidth}
height={documentHeight}
scale={{ x: scale, y: scale }}
shadowEnabled
shadowColor='black'
shadowOpacity={0.3}
shadowBlur={documentWidth * 0.05}
listening={false}
/>
{(isDrawingArea && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
</Layer>
{areAreasVisible
? <Layer id='areaLayer'>
<Areas scale={scale} />
</Layer>
: <></>
}
{areAreasVisible && areLinkAreaContextsVisible
? <Layer id='contextConnections'>
<ContextConnections />
</Layer>
: <></>
}
</Stage>
}
export default CanvasStage

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,30 @@
'use client'
import React from 'react'
import { Rect, } from 'react-konva'
type Props = {
rect: {
startX: number,
startY: number,
endX: number,
endY: number,
},
}
const DrawingArea = (props: Props) => {
const { rect } = props
const width = rect.endX - rect.startX
const height = rect.endY - rect.startY
return <Rect
width={width}
height={height}
x={rect.startX}
y={rect.startY}
strokeEnabled
stroke='#dc8dec'
strokeWidth={2}
strokeScaleEnabled={false}
/>
}
export default DrawingArea

View File

@ -1,82 +0,0 @@
import React, { useRef } from 'react'
import { ipc, entities } from '../../wailsjs/wailsjs/go/models'
import classNames from '../../utils/classNames'
import onEnterHandler from '../../utils/onEnterHandler'
import { useProject } from '../../context/Project/provider'
type Props = {
zoomLevel: number
processedArea?: entities.ProcessedArea
wordToEdit?: entities.ProcessedWord
setWordToEdit: (props?: { word: entities.ProcessedWord, areaId: string }) => void
setHoveredProcessedArea: (area?: entities.ProcessedArea) => void
}
const EditProcessedWord = ({ setWordToEdit, zoomLevel, wordToEdit, processedArea, setHoveredProcessedArea }: Props) => {
const {
requestUpdateProcessedWordById,
getProcessedAreaById,
} = useProject()
const editWordInput = useRef<HTMLInputElement>(null)
if (!wordToEdit || !processedArea) return <></>
const width = Math.floor((wordToEdit.boundingBox.x1 - wordToEdit.boundingBox.x0) * zoomLevel) + 2
const height = Math.floor(((wordToEdit.boundingBox.y1 - wordToEdit.boundingBox.y0) * zoomLevel) * 2) + 4
const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => {
requestUpdateProcessedWordById(wordId, newWordValue)
.then(res => {
getProcessedAreaById(processedArea.id || '').then(response => {
setHoveredProcessedArea(response)
})
})
.catch(console.error)
setWordToEdit(undefined)
}
return <div
dir={wordToEdit.direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'}
className={classNames('absolute inline-block p-1 rounded-md',
'bg-opacity-60 bg-black text-white',
)}
style={{
width,
height,
top: Math.floor(wordToEdit.boundingBox.y0 * zoomLevel) + (height / 2),
left: Math.floor(wordToEdit.boundingBox.x0 * zoomLevel)
}}
onBlur={() => setWordToEdit(undefined)}
>
<div
className={classNames('text-center align-middle block p-1 rounded-md shadow-zinc-900 shadow-2xl',
'bg-opacity-60 bg-black text-white',
)}
style={{
fontSize: `${3.4 * zoomLevel}vmin`,
height: height / 2,
}}>
{wordToEdit.fullText}
</div>
<input
type='text'
className='inline-block text-slate-900 p-0 m-0 w-full'
autoFocus
width={width}
ref={editWordInput}
placeholder={wordToEdit.fullText}
defaultValue={wordToEdit.fullText}
style={{
fontSize: `${3.4 * zoomLevel}vmin`,
height: height / 2,
}}
onFocus={(e) => e.currentTarget.select()}
onBlur={(e) => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value)}
onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(wordToEdit.id, e.currentTarget.value))}
/>
</div>
}
export default EditProcessedWord

View File

@ -0,0 +1,51 @@
import React from 'react'
import { Html } from 'react-konva-utils'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { useProject } from '../../context/Project/provider'
import onEnterHandler from '../../utils/onEnterHandler'
type Props = {
scale: number,
editingWord: entities.ProcessedWord,
setEditingWord: Function,
}
const EditingWord = (props: Props) => {
const { requestUpdateProcessedWordById } = useProject()
const { scale, setEditingWord, editingWord } = props
const handleWordCorrectionSubmit = (wordId: string, newWordValue: string) => {
requestUpdateProcessedWordById(wordId, newWordValue).catch(console.error)
setEditingWord(null)
}
const { x0, x1, y0, y1 } = editingWord.boundingBox
const left = x0 * scale
const top = y1 * scale
const width = (x1 - x0) * scale
const height = (y1 - y0) * scale
return <Html>
<input
defaultValue={editingWord.fullText}
style={{
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
textAlign: 'center',
display: 'block',
width: `${width}px`,
height: `${height}px`,
fontSize: `${Math.floor(24 * scale)}px`,
alignContent: 'center',
alignItems: 'center',
lineHeight: 0,
direction: 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr'
}}
onKeyDown={(e) => onEnterHandler(e, () => handleWordCorrectionSubmit(editingWord.id, e.currentTarget.value))}
onBlur={(e) => handleWordCorrectionSubmit(editingWord.id, e.currentTarget.value)}
/>
</Html>
}
export default EditingWord

View File

@ -1,55 +0,0 @@
'use client'
import React, { useEffect, useRef } from 'react'
import loadImage from '../../useCases/loadImage'
type Props = {
zoomLevel: number,
imagePath?: string,
setSize: (size: { width: number, height: number }) => void
}
const ImageCanvas = (props: Props) => {
const canvas = useRef<HTMLCanvasElement>(null)
const { imagePath, zoomLevel, setSize } = props
const applyImageToCanvas = async (path: string) => {
const canvasContext = canvas.current!.getContext('2d')!
let image: HTMLImageElement
try {
image = await loadImage(path)
} catch (err) {
return
}
const width = image.naturalWidth * zoomLevel
const height = image.naturalHeight * zoomLevel
updateSize({ width, height })
canvasContext.drawImage(image, 0, 0, width, height)
}
const clearCanvas = () => {
const canvasInstance = canvas.current!
const context = canvasInstance.getContext('2d')!
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
}
const updateSize = (size: { width: number, height: number }) => {
const canvasInstance = canvas.current!
const { width, height } = size
canvasInstance.width = width
canvasInstance.height = height
setSize(size)
}
useEffect(() => {
if (imagePath) applyImageToCanvas(imagePath)
}, [imagePath, zoomLevel])
return <canvas className="absolute" ref={canvas} />
}
export default ImageCanvas

View File

@ -0,0 +1,57 @@
'use client'
import React from 'react'
import { Group, Rect, Text } from 'react-konva'
import { entities } from '../../wailsjs/wailsjs/go/models'
type Props = {
area: entities.ProcessedArea,
word: entities.ProcessedWord,
scale: number,
setEditingWord: Function
}
const ProcessedWord = (props: Props) => {
const { area, scale, word, setEditingWord } = props
const { x0, x1, y0, y1 } = word.boundingBox
return <Group
id={word.id}
areaId={area.id}
isProcessedWord
onDblClick={() => setEditingWord(word)}>
<Rect
id={word.id}
areaId={area.id}
width={x1 - x0}
height={y1 - y0}
scale={{ x: scale, y: scale }}
x={x0 * scale}
y={y0 * scale}
strokeEnabled={false}
shadowForStrokeEnabled={false}
strokeScaleEnabled={false}
cornerRadius={4}
fill='rgb(80,80,80)'
opacity={0.4}
shadowColor='rgb(180,180,180)'
shadowBlur={10}
shadowOffset={{ x: 10, y: 10 }} />
<Text text={word.fullText}
width={x1 - x0}
height={y1 - y0}
scale={{ x: scale, y: scale }}
x={x0 * scale}
y={y0 * scale}
align='center'
verticalAlign='middle'
fontSize={36}
fontFamily='Calibri'
fill='white'
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
/>
</Group >
}
export default ProcessedWord

View File

@ -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

View 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

View File

@ -1,186 +0,0 @@
'use client'
import React, { WheelEvent, useEffect, useRef, useState } from 'react'
import { useProject } from '../../context/Project/provider'
import { entities } from '../../wailsjs/wailsjs/go/models'
import createUiCanvasInteractions from './createUiCanvasInteractions'
import processImageArea from '../../useCases/processImageArea'
import AreaTextPreview from './AreaTextPreview'
import EditProcessedWord from './EditProcessedWord'
type Props = {
width: number,
height: number
zoomDetails: { currentZoomLevel: number, zoomStep: number, maxZoomLevel: number }
setZoomLevel: (value: number) => void
}
let interactions: ReturnType<typeof createUiCanvasInteractions> | null = null
let downClickX = 0
let downClickY = 0
let isDrawing = false
const UiCanvas = (props: Props) => {
const {
getSelectedDocument,
getProcessedAreaById,
requestAddArea,
setSelectedAreaId,
} = useProject()
const canvas = useRef<HTMLCanvasElement>(null)
const [hoverOverAreaId, setHoverOverAreaId] = useState('')
const [wordToEdit, setWordToEdit] = useState<{ word: entities.ProcessedWord, areaId: string } | undefined>()
const [hoveredProcessedArea, setHoveredProcessedArea] = useState<entities.ProcessedArea | undefined>()
const areas = getSelectedDocument()?.areas || []
const { width, height, zoomDetails, setZoomLevel } = props
const { currentZoomLevel } = zoomDetails
const applyUiCanvasUpdates = () => {
const canvasContext = canvas.current!.getContext('2d')!
if (!areas || !areas.length) return
const hoverArea = areas.find(a => a.id === hoverOverAreaId)
if (!hoverArea) return
canvasContext.beginPath()
canvasContext.setLineDash([])
canvasContext.lineWidth = 6
canvasContext.strokeStyle = '#dc8dec'
const width = (hoverArea.endX - hoverArea.startX) * currentZoomLevel
const height = (hoverArea.endY - hoverArea.startY) * currentZoomLevel
const x = hoverArea.startX * currentZoomLevel
const y = hoverArea.startY * currentZoomLevel
canvasContext.roundRect(x, y, width, height, 4)
canvasContext.stroke()
canvasContext.closePath()
}
const clearCanvas = () => {
const canvasInstance = canvas.current!
const context = canvasInstance.getContext('2d')!
context.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
}
const handleMouseDown = (e: React.MouseEvent) => {
if (e.nativeEvent.shiftKey) {
downClickX = e.nativeEvent.offsetX
downClickY = e.nativeEvent.offsetY
isDrawing = true
}
}
const handleMouseMove = (e: React.MouseEvent) => {
if (isDrawing) interactions?.onActivelyDrawArea({
startX: downClickX,
startY: downClickY,
endX: e.nativeEvent.offsetX,
endY: e.nativeEvent.offsetY,
})
else interactions?.onHoverOverArea(
e.clientX,
e.clientY,
currentZoomLevel,
areas,
(areaId) => {
if (areaId === hoverOverAreaId) return
setHoverOverAreaId(areaId || '')
getProcessedAreaById(areaId || '').then(response => {
setHoveredProcessedArea(response)
})
}
)
}
const handleMouseUp = async (e: React.MouseEvent) => {
if (isDrawing) {
const coordinates = {
startMouseX: downClickX,
startMouseY: downClickY,
endMouseX: e.nativeEvent.offsetX,
endMouseY: e.nativeEvent.offsetY,
}
interactions?.onFinishDrawArea(coordinates, currentZoomLevel,
async (startX, startY, endX, endY) => {
const canvasInstance = canvas.current
if (!canvasInstance) return
const selectedDocumentId = getSelectedDocument()?.id
if (selectedDocumentId) {
const addedArea = await requestAddArea(selectedDocumentId, { startX, startY, endX, endY })
setSelectedAreaId(addedArea.id)
processImageArea(selectedDocumentId, addedArea.id)
}
const context = canvasInstance.getContext('2d')
context?.clearRect(0, 0, canvasInstance.width, canvasInstance.height)
isDrawing = false
downClickX = 0
downClickY = 0
}
)
}
}
const handleWheelEvent = (e: WheelEvent<HTMLCanvasElement>) => {
if (e.ctrlKey) interactions?.onZoom(e.deltaY, zoomDetails, setZoomLevel)
}
const updateSize = (size: { width: number, height: number }) => {
const canvasInstance = canvas.current!
const { width, height } = size
canvasInstance.width = width
canvasInstance.height = height
}
useEffect(() => {
if (!interactions && canvas.current) {
interactions = createUiCanvasInteractions(canvas.current)
}
}, [canvas.current])
useEffect(() => {
clearCanvas()
updateSize({ width, height })
applyUiCanvasUpdates()
}, [width, height, currentZoomLevel, areas])
useEffect(() => {
clearCanvas()
applyUiCanvasUpdates()
}, [hoverOverAreaId])
return <>
<canvas
className="absolute"
ref={canvas}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onWheel={handleWheelEvent}
/>
<AreaTextPreview
setWordToEdit={setWordToEdit}
processedArea={hoveredProcessedArea}
zoomLevel={currentZoomLevel}
areas={areas}
/>
<EditProcessedWord
zoomLevel={currentZoomLevel}
processedArea={hoveredProcessedArea}
wordToEdit={wordToEdit?.word}
setWordToEdit={setWordToEdit}
setHoveredProcessedArea={setHoveredProcessedArea}
/>
</>
}
export default UiCanvas

View File

@ -1,79 +0,0 @@
import isInBounds from '../../utils/isInBounds'
import { entities } from '../../wailsjs/wailsjs/go/models'
import { AddAreaToStoreCallback, HoverOverAreaCallback, MouseCoordinates, RectangleCoordinates, SetZoomCallback, ZoomDetails } from './types'
/**
* @param uiCanvas
* @returns Various methods to be called during events on the `UiCanvas`.
* Dependencies must be injected, such as state change callbacks.
*/
const createUiCanvasInteractions = (uiCanvas: HTMLCanvasElement) => {
const uiCanvasContext = uiCanvas.getContext('2d')!
return {
onActivelyDrawArea: (coordinates: RectangleCoordinates) => {
const { startX, startY, endX, endY } = coordinates
uiCanvasContext.clearRect(0, 0, uiCanvas.width, uiCanvas.height)
uiCanvasContext.beginPath()
const width = endX - startX
const height = endY - startY
uiCanvasContext.rect(startX, startY, width, height)
uiCanvasContext.strokeStyle = '#000'
uiCanvasContext.lineWidth = 2
uiCanvasContext.stroke()
},
onFinishDrawArea: (coordinates: MouseCoordinates, zoomLevel: number, addAreaToStoreCallback: AddAreaToStoreCallback) => {
let { startMouseX, endMouseX, startMouseY, endMouseY } = coordinates
let startX: number, endX: number
if (startMouseX < endMouseX) {
startX = Math.floor(startMouseX / zoomLevel)
endX = Math.floor(endMouseX / zoomLevel)
} else {
startX = Math.floor(endMouseX / zoomLevel)
endX = Math.floor(startMouseX / zoomLevel)
}
let startY: number, endY: number
if (startMouseY < endMouseY) {
startY = Math.floor(startMouseY / zoomLevel)
endY = Math.floor(endMouseY / zoomLevel)
} else {
startY = Math.floor(endMouseY / zoomLevel)
endY = Math.floor(startMouseY / zoomLevel)
}
addAreaToStoreCallback(startX, startY, endX, endY)
},
onZoom: (wheelDelta: number, zoomDetails: ZoomDetails, setZoomCallBack: SetZoomCallback) => {
const { currentZoomLevel, maxZoomLevel, zoomStep } = zoomDetails
const shouldAttemptToZoomIn = (wheelDelta < 0) && currentZoomLevel < maxZoomLevel
if (shouldAttemptToZoomIn) setZoomCallBack(currentZoomLevel + zoomStep)
else if (currentZoomLevel > (zoomStep * 2)) setZoomCallBack(currentZoomLevel - zoomStep)
},
onHoverOverArea: (mouseX: number, mouseY: number, zoomLevel: number, areas: entities.Area[], callback: HoverOverAreaCallback) => {
if (!areas.length) return
const domRect = uiCanvas.getBoundingClientRect()
const x = mouseX - domRect.left
const y = mouseY - domRect.top
const point = { x, y }
const areaContainingCoords = areas.find(a => {
const bounds = {
startX: a.startX,
startY: a.startY,
endX: a.endX,
endY: a.endY
}
return isInBounds(point, bounds, zoomLevel)
})
callback(areaContainingCoords?.id)
},
}
}
export default createUiCanvasInteractions

View File

@ -1,56 +1,34 @@
'use client' 'use client'
import React, { useState } from 'react' import dynamic from 'next/dynamic'
import { useProject, } from '../../context/Project/provider' import React, { useEffect, useRef } from 'react'
import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline' import { useDispatch } from 'react-redux'
import classNames from '../../utils/classNames' import ToolingOverlay from './ToolingOverlay'
import LanguageSelect from '../workspace/LanguageSelect' import { setSize } from '../../redux/features/stage/stageSlice'
import ImageCanvas from './ImageCanvas'
import AreaCanvas from './AreaCanvas'
import UiCanvas from './UiCanvas'
const zoomStep = 0.025 const CanvasStage = dynamic(() => import('./CanvasStage'), { ssr: false })
const maxZoomLevel = 4
const DocumentCanvas = () => { const DocumentCanvas = () => {
const { getSelectedDocument } = useProject() const dispatch = useDispatch()
const selectedDocument = getSelectedDocument()
const [zoomLevel, setZoomLevel] = useState(1) const thisRef = useRef<HTMLDivElement>(null)
const [size, setSize] = useState({ width: 0, height: 0 })
const { width, height } = size
return <div className='relative'> const handleWindowResize = () => {
<div className='flex justify-between align-top mb-2'> const width = thisRef?.current?.clientWidth || 0
<div className='flex align-top'> const height = thisRef?.current?.clientHeight || 0
<h1 className="text-xl font-semibold text-gray-900 inline-block mr-2">{selectedDocument?.name}</h1> dispatch(setSize({ width, height }))
<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={classNames('relative mt-2 overflow-scroll',
'w-[calc(100vw-320px)] h-[calc(100vh-174px)] border-4',
'border-dashed border-gray-200')}>
<ImageCanvas imagePath={selectedDocument?.path} zoomLevel={zoomLevel} setSize={setSize} /> useEffect(() => {
<AreaCanvas width={width} height={height} zoomLevel={zoomLevel} /> handleWindowResize()
<UiCanvas window.addEventListener('resize', handleWindowResize)
width={width} return () => window.removeEventListener('resize', handleWindowResize)
height={height} }, [thisRef?.current?.clientWidth, thisRef?.current?.clientHeight])
setZoomLevel={setZoomLevel}
zoomDetails={{ return <div ref={thisRef} className='relative' style={{ height: 'calc(100vh - 140px)' }}>
currentZoomLevel: zoomLevel, <div className='h-full overflow-hidden rounded-lg border-4 border-dashed border-gray-200'>
maxZoomLevel: maxZoomLevel, <CanvasStage />
zoomStep: zoomStep, <ToolingOverlay />
}} />
</div> </div>
</div > </div >
} }

View File

@ -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

View 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

View File

@ -17,7 +17,7 @@ const MainProject = () => {
const [isProjectListModal, setIsProjectListModal] = useState(false) const [isProjectListModal, setIsProjectListModal] = useState(false)
const [canPopoverBeOpen, setCanPopoverBeOpen] = useState(true) const [canPopoverBeOpen, setCanPopoverBeOpen] = useState(true)
const [avalibleProjects, setAvalibleProjects] = useState<entities.Project[]>([]) const [availableProjects, setAvailableProjects] = useState<entities.Project[]>([])
const { createNewProject, requestSelectProjectByName } = useProject() const { createNewProject, requestSelectProjectByName } = useProject()
const { setSelectedMainPage } = useNavigation() const { setSelectedMainPage } = useNavigation()
@ -39,7 +39,7 @@ const MainProject = () => {
setCanPopoverBeOpen(false) setCanPopoverBeOpen(false)
GetAllLocalProjects().then(response => { GetAllLocalProjects().then(response => {
console.log(response) console.log(response)
setAvalibleProjects(response) setAvailableProjects(response)
setIsProjectListModal(true) setIsProjectListModal(true)
}) })
}, },
@ -73,7 +73,7 @@ const MainProject = () => {
{isNewProjectModalOpen ? <NewProjectModal onCreateNewProjectHandler={onCreateNewProjectHandler} /> : ''} {isNewProjectModalOpen ? <NewProjectModal onCreateNewProjectHandler={onCreateNewProjectHandler} /> : ''}
{isProjectListModal ? <ProjectListModal onSelectProjectHandler={onSelectProjectHandler} projects={avalibleProjects} /> : '' } {isProjectListModal ? <ProjectListModal onSelectProjectHandler={onSelectProjectHandler} projects={availableProjects} /> : '' }
<div className="py-20 px-6 sm:px-6 sm:py-32 lg:px-8"> <div className="py-20 px-6 sm:px-6 sm:py-32 lg:px-8">
<div className="mx-auto max-w-2xl text-center"> <div className="mx-auto max-w-2xl text-center">

View 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

View File

@ -11,10 +11,12 @@ const MainWorkspace = () => {
const { getSelectedDocument, selectedDocumentId } = useProject() const { getSelectedDocument, selectedDocumentId } = useProject()
const { selectedWorkspace } = useNavigation() const { selectedWorkspace } = useNavigation()
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">
<div className='flex-1'> <div className='flex-1'>
@ -26,7 +28,7 @@ const renderSelectedWorkSpace = () => {
Image Processor Image Processor
</h1> : ''} </h1> : ''}
</div> </div>
{ renderSelectedWorkSpace() } {renderSelectedWorkSpace()}
</div> </div>
</div> </div>
</div> </div>

View File

@ -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
@ -46,7 +45,8 @@ const AreaLineItem = (props: { area: SidebarArea, documentId: string, index: num
const onAreaClick = (areaId: string) => { const onAreaClick = (areaId: string) => {
setSelectedDocumentId(props.documentId) setSelectedDocumentId(props.documentId)
setSelectedAreaId(areaId) if (selectedAreaId !== areaId) setSelectedAreaId(areaId)
else setSelectedAreaId('')
} }
const onAreaDoubleClick = (areaId: string) => { const onAreaDoubleClick = (areaId: string) => {
@ -125,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>

View File

@ -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
View File

@ -0,0 +1,7 @@
const colors = {
BRAND_PRIMARY: {
hex: '#dc8dec',
}
}
export { colors }

View File

@ -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)

View File

@ -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,
} }

View File

@ -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

View File

@ -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 = {}

View File

@ -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

View File

@ -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}>

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,19 @@
"@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",
"next": "^13.1.1", "konva": "^9.2.0",
"next": "^13.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-konva": "^18.2.9",
"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",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1 +1 @@
2415a78ef8f325df057b22f577cbbe50 bf8d6eeb2add78baa4092415a836f1ad

View File

@ -3,15 +3,16 @@
import { AppProps } from 'next/app' import { AppProps } from 'next/app'
import { ProjectProvider } from '../context/Project/provider' import { ProjectProvider } from '../context/Project/provider'
import '../styles/globals.css' import '../styles/globals.css'
import { ipc } 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: '',
documents: [] as ipc.Document[], documents: [] as entities.Document[],
groups: [] as ipc.Group[] groups: [] as entities.Group[]
} }
const initialNavigationProps = { const initialNavigationProps = {
@ -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>
} }

View File

@ -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 />
</> </>
} }

Binary file not shown.

View File

@ -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

View 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
}

View 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

View 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
View 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

View 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
View 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)

View File

@ -16,4 +16,7 @@ module.exports = {
}, },
}, },
}, },
colors: {
brandPrimary: '#dc8dec',
}
} }

View File

@ -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,15 +9,27 @@ 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')
const { path } = foundDocument const { path } = foundDocument
const imageData = await loadImage(path) 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)
const scheduler = createScheduler() const scheduler = createScheduler()
const worker = await createWorker()
await worker.loadLanguage(processLanguage) await worker.loadLanguage(processLanguage)
await worker.initialize(processLanguage) await worker.initialize(processLanguage)
scheduler.addWorker(worker) scheduler.addWorker(worker)
@ -31,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,
@ -39,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,
@ -60,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

View 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

View File

@ -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,
} }

View File

@ -0,0 +1,6 @@
const asyncClick = (e: React.MouseEvent, callback: (e: React.MouseEvent) => Promise<void>) => {
e.preventDefault()
callback(e)
}
export default asyncClick

View File

@ -0,0 +1,30 @@
import { RectangleCoordinates } from '../components/DocumentCanvas/types'
const getNormalizedRectToBounds = (rect: RectangleCoordinates, width: number, height: number, scale: number = 1): RectangleCoordinates => {
let startX: number, endX: number
if (rect.startX < rect.endX) {
startX = Math.floor(rect.startX / scale)
endX = Math.floor(rect.endX / scale)
} else {
startX = Math.floor(rect.endX / scale)
endX = Math.floor(rect.startX / scale)
}
let startY: number, endY: number
if (rect.startY < rect.endY) {
startY = Math.floor(rect.startY / scale)
endY = Math.floor(rect.endY / scale)
} else {
startY = Math.floor(rect.endY / scale)
endY = Math.floor(rect.startY / scale)
}
if (startX < 0) startX = 0
if (startY < 0) startY = 0
if (endX > width) endX = width
if (endY > height) endY = height
return { startX, startY, endX, endY }
}
export default getNormalizedRectToBounds

View File

@ -1,7 +1,7 @@
import { GetSuppportedLanguages } from '../wailsjs/wailsjs/go/ipc/Channel' import { GetSupportedLanguages } from '../wailsjs/wailsjs/go/ipc/Channel'
const getSupportedLanguages = async () => { const getSupportedLanguages = async () => {
const response = await GetSuppportedLanguages() const response = await GetSupportedLanguages()
return response return response
} }

View File

@ -17,11 +17,15 @@ 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 GetSuppportedLanguages():Promise<Array<entities.Language>>; export function GetSerializedContextGroups():Promise<Array<entities.SerializedLinkedProcessedArea>>;
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>;

View File

@ -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,8 +42,12 @@ export function GetProjectByName(arg1) {
return window['go']['ipc']['Channel']['GetProjectByName'](arg1); return window['go']['ipc']['Channel']['GetProjectByName'](arg1);
} }
export function GetSuppportedLanguages() { export function GetSerializedContextGroups() {
return window['go']['ipc']['Channel']['GetSuppportedLanguages'](); return window['go']['ipc']['Channel']['GetSerializedContextGroups']();
}
export function GetSupportedLanguages() {
return window['go']['ipc']['Channel']['GetSupportedLanguages']();
} }
export function GetUserMarkdownByDocumentId(arg1) { export function GetUserMarkdownByDocumentId(arg1) {
@ -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);
} }

View File

@ -4,6 +4,7 @@ export namespace entities {
displayName: string; displayName: string;
processCode: string; processCode: string;
translateCode: string; translateCode: string;
isBundledCustom: boolean;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new Language(source); return new Language(source);
@ -14,6 +15,7 @@ export namespace entities {
this.displayName = source["displayName"]; this.displayName = source["displayName"];
this.processCode = source["processCode"]; this.processCode = source["processCode"];
this.translateCode = source["translateCode"]; this.translateCode = source["translateCode"];
this.isBundledCustom = source["isBundledCustom"];
} }
} }
export class Area { export class Area {
@ -24,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 = {}) {
@ -39,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"];
} }
@ -237,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;
@ -250,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"];
@ -276,7 +282,6 @@ export namespace entities {
} }
} }
export class ProcessedLine { export class ProcessedLine {
fullText: string;
words: ProcessedWord[]; words: ProcessedWord[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
@ -285,7 +290,6 @@ 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.fullText = source["fullText"];
this.words = this.convertValues(source["words"], ProcessedWord); this.words = this.convertValues(source["words"], ProcessedWord);
} }
@ -310,7 +314,6 @@ export namespace entities {
export class ProcessedArea { export class ProcessedArea {
id: string; id: string;
documentId: string; documentId: string;
fullText: string;
order: number; order: number;
lines: ProcessedLine[]; lines: ProcessedLine[];
@ -322,7 +325,6 @@ export namespace entities {
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.documentId = source["documentId"]; this.documentId = source["documentId"];
this.fullText = source["fullText"];
this.order = source["order"]; this.order = source["order"];
this.lines = this.convertValues(source["lines"], ProcessedLine); this.lines = this.convertValues(source["lines"], ProcessedLine);
} }
@ -420,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;
@ -479,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);
@ -488,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 {

View File

@ -225,3 +225,11 @@ export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show) // [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application. // Shows the application.
export function Show(): void; export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;

View File

@ -37,11 +37,11 @@ export function LogFatal(message) {
} }
export function EventsOnMultiple(eventName, callback, maxCallbacks) { export function EventsOnMultiple(eventName, callback, maxCallbacks) {
window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
} }
export function EventsOn(eventName, callback) { export function EventsOn(eventName, callback) {
EventsOnMultiple(eventName, callback, -1); return EventsOnMultiple(eventName, callback, -1);
} }
export function EventsOff(eventName, ...additionalEventNames) { export function EventsOff(eventName, ...additionalEventNames) {
@ -49,7 +49,7 @@ export function EventsOff(eventName, ...additionalEventNames) {
} }
export function EventsOnce(eventName, callback) { export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1); return EventsOnMultiple(eventName, callback, 1);
} }
export function EventsEmit(eventName) { export function EventsEmit(eventName) {
@ -192,3 +192,11 @@ export function Hide() {
export function Show() { export function Show() {
window.runtime.Show(); window.runtime.Show();
} }
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}

10
go.mod
View File

@ -6,13 +6,13 @@ go 1.18
require ( require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/wailsapp/wails/v2 v2.3.1 github.com/snakesel/libretranslate v0.0.2
github.com/wailsapp/wails/v2 v2.5.1
) )
require ( require (
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.9.0 // indirect github.com/labstack/echo/v4 v4.9.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect github.com/labstack/gommon v0.4.0 // indirect
@ -30,7 +30,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.4.0 // indirect golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f // indirect golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect
) )

21
go.sum
View File

@ -7,8 +7,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
@ -40,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=
@ -52,15 +52,15 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.3.1 h1:ZJz+pyIBKyASkgO8JO31NuHO1gTTHmvwiHYHwei1CqM= github.com/wailsapp/wails/v2 v2.5.1 h1:mfG+2kWqQXYOwdgI43HEILjOZDXbk5woPYI3jP2b+js=
github.com/wailsapp/wails/v2 v2.3.1/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8= github.com/wailsapp/wails/v2 v2.5.1/go.mod h1:jbOZbcr/zm79PxXxAjP8UoVlDd9wLW3uDs+isIthDfs=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f h1:90Jq/vvGVDsqj8QqCynjFw9MCerDguSMODLYII416Y8= golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f h1:90Jq/vvGVDsqj8QqCynjFw9MCerDguSMODLYII416Y8=
golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20221207211629-99ab8fa1c11f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -70,15 +70,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -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
View 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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
@ -192,7 +199,7 @@ func (c *Channel) RequestChangeSessionProjectByName(projectName string) bool {
return session.GetInstance().Project.Id == foundProject.Id return session.GetInstance().Project.Id == foundProject.Id
} }
func (c *Channel) GetSuppportedLanguages() []entities.Language { func (c *Channel) GetSupportedLanguages() []entities.Language {
supportedLanguages := consts.GetSuppportedLanguages() supportedLanguages := consts.GetSupportedLanguages()
return supportedLanguages return supportedLanguages
} }

View 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
}

View File

@ -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
View 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 ("")
}
}