feat: starting features of translation & contexts

This commit is contained in:
Joshua Shoemaker 2023-07-24 07:45:03 -05:00
parent 917662e9ba
commit f129a9cb13
30 changed files with 762 additions and 89 deletions

View File

@ -6,6 +6,7 @@
"headlessui",
"heroicons",
"konva",
"libretranslate",
"Tesseract",
"wailsjs"
]

163
entities/ContextGroup.go Normal file
View File

@ -0,0 +1,163 @@
package entities
import (
"errors"
"fmt"
"github.com/google/uuid"
)
type IndependentTranslatedWord struct {
Id string
ProcessedWordId string
Value string
}
type LinkedProcessedArea struct {
Area ProcessedArea
previous *LinkedProcessedArea
next *LinkedProcessedArea
}
type LinkedAreaList struct {
head *LinkedProcessedArea
tail *LinkedProcessedArea
}
func (l *LinkedAreaList) First() *LinkedProcessedArea {
return l.head
}
func (linkedProcessedWord *LinkedProcessedArea) Next() *LinkedProcessedArea {
return linkedProcessedWord.next
}
func (linkedProcessedWord *LinkedProcessedArea) Prev() *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.Next() {
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
}
type ContextGroup struct { // TODO: possibly remove this and expand the LinkedAreaList struct instead
Id string
DocumentId string
LinkedAreaList LinkedAreaList
TranslationText string
}
type ContextGroupCollection struct { // TODO: these methods should live in core not entitites
Groups []ContextGroup
}
var contextGroupCollectionInstance *ContextGroupCollection
func GetContextGroupCollection() *ContextGroupCollection {
if contextGroupCollectionInstance == nil {
contextGroupCollectionInstance = &ContextGroupCollection{}
}
return contextGroupCollectionInstance
}
func SetContextGroupCollection(collection ContextGroupCollection) *ContextGroupCollection {
contextGroupCollectionInstance = &collection
return contextGroupCollectionInstance
}
func (collection *ContextGroupCollection) FindContextGroupByNodeId(id string) *ContextGroup {
var foundContextGroup *ContextGroup
for i, g := range collection.Groups {
if g.LinkedAreaList.Find(id) != nil {
foundContextGroup = &collection.Groups[i]
break
}
}
return foundContextGroup
}
func (collection *ContextGroupCollection) CreateContextGroupFromProcessedArea(area ProcessedArea) bool {
fmt.Println("CreateContextGroupFromProcessedArea")
newLinkedAreaList := LinkedAreaList{}
newLinkedAreaList.Push(area)
newContextGroup := ContextGroup{
Id: uuid.NewString(),
DocumentId: area.DocumentId,
LinkedAreaList: newLinkedAreaList,
}
collection.Groups = append(collection.Groups, newContextGroup)
return true
}
// TODO: completely rework this linked list and the collection
func (collection *ContextGroupCollection) ConnectAreaAsTailToNode(tailArea ProcessedArea, headArea ProcessedArea) bool {
headNodeContextGroup := collection.FindContextGroupByNodeId(headArea.Id)
if headNodeContextGroup == nil {
collection.CreateContextGroupFromProcessedArea(headArea)
headNodeContextGroup = collection.FindContextGroupByNodeId(headArea.Id)
}
headNode := headNodeContextGroup.LinkedAreaList.Find(headArea.Id)
headNode.next = &LinkedProcessedArea{
Area: tailArea,
previous: headNode,
}
return true
}

View File

@ -23,5 +23,6 @@ type Area struct {
EndX int `json:"endX"`
EndY int `json:"endY"`
Language Language `json:"language"`
TranslateLanguage Language `json:"translateLanguage"`
Order int `json:"order"`
}

View File

@ -15,6 +15,7 @@ type ProcessedSymbol struct {
type ProcessedWord struct {
Id string `json:"id"`
AreaId string `json:"areaId"`
FullText string `json:"fullText"`
Symbols []ProcessedSymbol `json:"symbols"`
Confidence float32 `json:"confidence"`

View File

@ -1,17 +1,17 @@
'use client'
import React, { useState } from 'react'
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 { KonvaEventObject } from 'konva/lib/Node'
import Konva from 'konva'
import AreaContextMenu from './AreaContextMenu'
import { useStage } from './context/provider'
type Props = {
isActive: boolean,
area: entities.Area,
scale: number,
setHoveredOverAreaIds: Function
setHoveredProcessedArea: Function
}
@ -20,11 +20,12 @@ type coordinates = { x: number, y: number }
const Area = (props: Props) => {
const { getProcessedAreaById, selectedAreaId, setSelectedAreaId } = useProject()
const { scale } = useStage()
const shapeRef = React.useRef<Konva.Rect>(null)
const [isAreaContextMenuOpen, setIsAreaContextMenuOpen] = useState(false)
const [areaContextMenuPosition, setAreaContextMenuPosition] = useState<coordinates>()
const { area, scale, isActive, setHoveredOverAreaIds, setHoveredProcessedArea } = props
const { area, isActive, setHoveredOverAreaIds, setHoveredProcessedArea } = props
const a = area
const width = (a.endX - a.startX)
const height = (a.endY - a.startY)
@ -79,15 +80,16 @@ const Area = (props: Props) => {
onMouseLeave={handleEnterOrLeave}
onClick={() => handleAreaClick(a.id)}
onContextMenu={handleContextMenu}
isArea />
{!isAreaContextMenuOpen
? <></>
: <AreaContextMenu
isArea
/>
{isAreaContextMenuOpen
? <AreaContextMenu
area={area}
x={areaContextMenuPosition?.x || 0}
y={areaContextMenuPosition?.y || 0}
scale={scale}
setIsAreaContextMenuOpen={setIsAreaContextMenuOpen} />
: <></>
}
</Group>
}

View File

@ -11,6 +11,7 @@ import processImageArea from '../../../useCases/processImageArea'
import classNames from '../../../utils/classNames'
import { useNotification } from '../../../context/Notification/provider'
import LanguageSelect from '../../utils/LanguageSelect'
import { RequestTranslateArea } from '../../../wailsjs/wailsjs/go/ipc/Channel'
type Props = {
x: number,
@ -77,6 +78,19 @@ const AreaContextMenu = (props: Props) => {
}
}
const handleTranslateArea = async () => {
setIsAreaContextMenuOpen(false)
try {
const wasSuccessful = await RequestTranslateArea(area.id)
if (wasSuccessful) addNotificationToQueue({ message: 'Successfully translated area' })
else addNotificationToQueue({ message: 'Issue translating area', level: 'warning' })
} catch (err) {
addNotificationToQueue({ message: 'Error translating area', level: 'error' })
}
}
const handleProcessLanguageSelect = async (selectedLanguage: entities.Language) => {
setIsAreaContextMenuOpen(false)
@ -88,7 +102,6 @@ const AreaContextMenu = (props: Props) => {
return
}
const selectedDocumentId = getSelectedDocument()?.id
if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) {
addNotificationToQueue({ message: 'Did not successfully update area language', level: 'warning' })
@ -114,7 +127,7 @@ const AreaContextMenu = (props: Props) => {
return <Html>
<div style={makeFormStyles(x, y, scale)} tabIndex={1} onBlur={handleOnBlur}>
<div className={classNames(
'z-40 min-w-max py-1 rounded-md shadow-sm outline-none font-light',
'z-40 min-w-max py-1 rounded-lg shadow-sm outline-none font-light',
'bg-white border border-gray-200',)}
>
@ -136,6 +149,17 @@ const AreaContextMenu = (props: Props) => {
<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,

View File

@ -7,11 +7,13 @@ import { entities } from '../../wailsjs/wailsjs/go/models'
import Area from './Area'
import ProcessedWord from './ProcessedWord'
import EditingWord from './EditingWord'
import { useStage } from './context/provider'
type Props = { scale: number }
const Areas = ({ scale }: Props) => {
const { getSelectedDocument, selectedAreaId } = useProject()
const { isProcessedWordsVisible } = useStage()
const areas = getSelectedDocument()?.areas || []
const [hoveredOverAreaIds, setHoveredOverAreaIds] = useState<string[]>([])
const [hoveredProcessedAreas, setHoveredProcessedArea] = useState<entities.ProcessedArea[]>([])
@ -44,7 +46,6 @@ const Areas = ({ scale }: Props) => {
const renderAreas = (areas: entities.Area[]) => areas.map((a, index) => {
return <Area key={index}
area={a}
scale={scale}
setHoveredOverAreaIds={setHoveredOverAreaIds}
setHoveredProcessedArea={setHoveredProcessedArea}
isActive={(hoveredOverAreaIds.includes(a.id) || a.id === selectedAreaId)} />
@ -52,8 +53,8 @@ const Areas = ({ scale }: Props) => {
return <Group>
{renderAreas(areas)}
{renderProcessedWords()}
{renderEditingWord()}
{isProcessedWordsVisible ? renderProcessedWords() : <></>}
{isProcessedWordsVisible ? renderEditingWord() : <></>}
</Group>
}

View File

@ -10,21 +10,15 @@ import { RectangleCoordinates } from './types'
import DrawingArea from './DrawingArea'
import getNormalizedRectToBounds from '../../utils/getNormalizedRectToBounds'
import processImageArea from '../../useCases/processImageArea'
type Props = {
scale: number,
scaleStep: number,
maxScale: number,
setScale: Function,
size: { width: number, height: number }
}
import { useStage } from './context/provider'
import ContextConnections from './ContextConnections'
let downClickX: number
let downClickY: number
let isDrawing = false
const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
const CanvasStage = () => {
const { getSelectedDocument, requestAddArea, setSelectedAreaId } = useProject()
const { scale, scaleStep, maxScale, size, setScale, isAreasVisible, isLinkAreaContextsVisible, isDrawingArea, setIsDrawingArea, startingContextConnection, setStartingContextConnection } = useStage()
const [documentImage] = useImage(getSelectedDocument()?.path || '')
const documentRef = useRef(null)
const [drawingAreaRect, setDrawingAreaRect] = useState<RectangleCoordinates | null>(null)
@ -33,17 +27,18 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
const documentHeight = documentImage?.naturalHeight || 0
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
if (startingContextConnection) return setStartingContextConnection(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
isDrawing = true
setIsDrawingArea(true)
}
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
const currentPosition = e.currentTarget.getRelativePointerPosition()
if (isDrawing) return setDrawingAreaRect({
if (isDrawingArea) return setDrawingAreaRect({
startX: downClickX,
startY: downClickY,
endX: currentPosition.x,
@ -54,7 +49,7 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
const handleMouseUp = (e: KonvaEventObject<MouseEvent>) => {
const stage = e.currentTarget
if (stage.isDragging()) stage.stopDrag()
if (isDrawing) isDrawing = false
else if (isDrawingArea) setIsDrawingArea(false)
if (!drawingAreaRect) return
@ -92,11 +87,20 @@ const CanvasStage = ({ scale, scaleStep, maxScale, setScale, size }: Props) => {
shadowBlur={documentWidth * 0.05}
listening={false}
/>
{(isDrawing && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
{(isDrawingArea && drawingAreaRect) ? <DrawingArea rect={drawingAreaRect} /> : <></>}
</Layer>
<Layer>
{isAreasVisible
? <Layer id='areaLayer'>
<Areas scale={scale} />
</Layer>
: <></>
}
{isAreasVisible && isLinkAreaContextsVisible
? <Layer id='contextConnections'>
<ContextConnections />
</Layer>
: <></>
}
</Stage>
}

View File

@ -0,0 +1,100 @@
'use client'
import { Circle, Group } from 'react-konva'
import { useStage } from '../context/provider'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { KonvaEventObject } from 'konva/lib/Node'
import { RequestConnectAreaAsTailToNode } from '../../../wailsjs/wailsjs/go/ipc/Channel'
type Props = { areas: entities.Area[] }
const ConnectionPoints = (props: Props) => {
const { isLinkAreaContextsVisible, scale, startingContextConnection, setStartingContextConnection } = useStage()
const handleContextAreaMouseDown = (e: KonvaEventObject<MouseEvent>) => {
e.cancelBubble = true
const clickedConnectionPoint = {
isHead: e.currentTarget.attrs.isHead,
areaId: e.currentTarget.attrs.id
}
if (!startingContextConnection) return setStartingContextConnection(clickedConnectionPoint)
if (clickedConnectionPoint.isHead === startingContextConnection.isHead
|| clickedConnectionPoint.areaId === startingContextConnection.areaId)
return setStartingContextConnection(null)
console.log('connected points', startingContextConnection, clickedConnectionPoint)
const headId = clickedConnectionPoint.isHead ? clickedConnectionPoint.areaId : startingContextConnection.areaId
const tailId = !clickedConnectionPoint.isHead ? startingContextConnection.areaId : clickedConnectionPoint.areaId
RequestConnectAreaAsTailToNode(headId, tailId).then(res => console.log(res)).catch(err => console.warn(err))
}
const renderConnectingPointsForArea = (a: entities.Area) => {
if (!isLinkAreaContextsVisible) return <></>
const headConnector = <Circle
key={`head-${a.id}`}
id={a.id}
radius={10}
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 (!startingContextConnection) connectorsToRender = [headConnector, tailConnector]
else if (startingContextConnection.isHead) connectorsToRender = [tailConnector]
else connectorsToRender = [headConnector]
if (startingContextConnection?.areaId === a.id) {
let y = (startingContextConnection.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={startingContextConnection.isHead ? '#dc8dec' : '#1e1e1e'}
strokeScaleEnabled={false}
shadowForStrokeEnabled={false}
isHead={startingContextConnection.isHead}
onMouseDown={() => setStartingContextConnection(null)}
/>)
}
return <Group>
{connectorsToRender}
</Group>
}
const renderAllConnectingPoints = () => props.areas.map(a => renderConnectingPointsForArea(a))
return <Group>
{renderAllConnectingPoints()}
</Group>
}
export default ConnectionPoints

View File

@ -0,0 +1,55 @@
'use client'
import React from 'react'
import { Line } from 'react-konva'
import { StartingContextConnection } from '../context/types'
import { entities } from '../../../wailsjs/wailsjs/go/models'
import { Coordinates } from '../types'
type CurrentDrawingConnectionProps = {
startingContextConnection: StartingContextConnection | null
areas: entities.Area[],
scale: number,
endDrawingPosition: Coordinates | null
}
const CurrentDrawingConnection = (props: CurrentDrawingConnectionProps) => {
const { startingContextConnection, areas, scale, endDrawingPosition } = props
if (!startingContextConnection || !endDrawingPosition) return <></>
const { areaId, isHead } = startingContextConnection
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}
/>
}
export default CurrentDrawingConnection

View File

@ -0,0 +1,41 @@
'use client'
import React, { useEffect, useState } from 'react'
import { Group } from 'react-konva'
import { useStage } from '../context/provider'
import { useProject } from '../../../context/Project/provider'
import Konva from 'konva'
import { Coordinates } from '../types'
import CurrentDrawingConnection from './CurrentDrawingConnection'
import ConnectionPoints from './ConnectionPoints'
const ContextConnections = () => {
const { getSelectedDocument } = useProject()
const { isLinkAreaContextsVisible, startingContextConnection, setStartingContextConnection, scale } = useStage()
const areas = getSelectedDocument()?.areas || []
const [endDrawingPosition, setEndDrawingPosition] = useState<Coordinates | null>(null)
const handleMouseMove = (e: MouseEvent) => {
if (!isLinkAreaContextsVisible || !startingContextConnection) return
setEndDrawingPosition(Konva.stages[0].getRelativePointerPosition())
}
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
})
useEffect(() => {
if (!startingContextConnection) setEndDrawingPosition(null)
}, [startingContextConnection])
if (!isLinkAreaContextsVisible) return <></>
return <Group>
<ConnectionPoints areas={areas} />
<CurrentDrawingConnection areas={areas} startingContextConnection={startingContextConnection} endDrawingPosition={endDrawingPosition} scale={scale} />
</Group>
}
export default ContextConnections

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 pointer-events-auto">
<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,68 @@
'use client'
import React, { useEffect, useState } from 'react'
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 { useStage } from '../context/provider'
import ToolToggleButton from './ToolToggleButton'
const ToolingOverlay = () => {
const { getSelectedDocument, selectedAreaId, } = useProject()
const {
scale, scaleStep, maxScale, setScale,
isLinkAreaContextsVisible, setIsLinkAreaContextsVisible,
isAreasVisible, setIsAreasVisible,
isProcessedWordsVisible, setIsProcessedWordsVisible,
isTranslatedWordsVisible, setIsTranslatedWordsVisible,
} = useStage()
const selectedDocument = getSelectedDocument()
const [selectedArea, setSelectedArea] = useState<entities.Area | undefined>()
useEffect(() => {
setSelectedArea(selectedDocument?.areas.find(a => a.id == selectedAreaId))
}, [selectedAreaId])
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>
<LanguageSelect styles={{ fontSize: '16px', borderRadius: '2px' }} defaultLanguage={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) => { setScale(e.currentTarget.valueAsNumber) }}
/>
<MagnifyingGlassPlusIcon className='w-4 h-4' />
</div>
</div>
{/* Right Buttons */}
<div className='absolute bottom-6 right-3 pointer-events-none'>
{isAreasVisible
? <>
<ToolToggleButton icon={LinkIcon} hint='Link Area Contexts' isActive={isLinkAreaContextsVisible} onClick={() => setIsLinkAreaContextsVisible(!isLinkAreaContextsVisible)} />
<ToolToggleButton icon={LanguageIcon} hint='Toggle Translations' isActive={isTranslatedWordsVisible} onClick={() => setIsTranslatedWordsVisible(!isTranslatedWordsVisible)} />
<ToolToggleButton icon={DocumentTextIcon} hint='Toggle Processed' isActive={isProcessedWordsVisible} onClick={() => setIsProcessedWordsVisible(!isProcessedWordsVisible)} />
</>
: <></>
}
<ToolToggleButton icon={SquaresPlusIcon} hint='Toggle Areas' isActive={isAreasVisible} onClick={() => setIsAreasVisible(!isAreasVisible)} />
</div>
</>
}
export default ToolingOverlay

View File

@ -0,0 +1,24 @@
import { StageContextType } from './types'
const makeDefaultStage = (): StageContextType => ({
scale: 1,
maxScale: 4,
scaleStep: 0.01,
setScale: (_) => {},
isAreasVisible: true,
setIsAreasVisible: (_) => {},
isProcessedWordsVisible: true,
setIsProcessedWordsVisible: (_) => {},
isTranslatedWordsVisible: true,
setIsTranslatedWordsVisible: (_) => {},
isLinkAreaContextsVisible: false,
setIsLinkAreaContextsVisible: (_) => {},
size: { width: 1, height: 1 },
setSize: (_) => {},
isDrawingArea: false,
setIsDrawingArea: (_) => {},
startingContextConnection: null,
setStartingContextConnection: (_) => {},
})
export default makeDefaultStage

View File

@ -0,0 +1,51 @@
'use client'
import { createContext, useContext, useState, ReactNode, } from 'react'
import makeDefaultStage from './makeDefaultStage'
import { StageContextType, StartingContextConnection } from './types'
const StageContext = createContext<StageContextType>(makeDefaultStage())
export function useStage() {
return useContext(StageContext)
}
const maxScale = 4
const scaleStep = 0.01
type Props = { children: ReactNode }
export function StageProvider({ children }: Props) {
const [size, setSize] = useState({width: 1, height: 1})
const [scale, setScale] = useState(1)
const [isAreasVisible, setIsAreasVisible] = useState(true)
const [isProcessedWordsVisible, setIsProcessedWordsVisible] = useState(true)
const [isTranslatedWordsVisible, setIsTranslatedWordsVisible] = useState(true)
const [isLinkAreaContextsVisible, setIsLinkAreaContextsVisible] = useState(false)
const [isDrawingArea, setIsDrawingArea] = useState(false)
const [startingContextConnection, setStartingContextConnection] = useState<StartingContextConnection | null>(null)
const value = {
scale,
maxScale,
scaleStep,
setScale,
isAreasVisible,
setIsAreasVisible,
isProcessedWordsVisible,
setIsProcessedWordsVisible,
isTranslatedWordsVisible,
setIsTranslatedWordsVisible,
isLinkAreaContextsVisible,
setIsLinkAreaContextsVisible,
size,
setSize,
isDrawingArea,
setIsDrawingArea,
startingContextConnection,
setStartingContextConnection,
}
return <StageContext.Provider value={value}>
{ children }
</StageContext.Provider>
}

View File

@ -0,0 +1,25 @@
export type StartingContextConnection = {
isHead: boolean,
areaId: string,
}
export type StageContextType = {
scale: number,
maxScale: number,
scaleStep: number,
setScale: (value: number) => void,
isAreasVisible: boolean,
setIsAreasVisible: (value: boolean) => void,
isProcessedWordsVisible: boolean,
setIsProcessedWordsVisible: (value: boolean) => void,
isTranslatedWordsVisible: boolean,
setIsTranslatedWordsVisible: (value: boolean) => void,
isLinkAreaContextsVisible: boolean,
setIsLinkAreaContextsVisible: (value: boolean) => void,
size: { width: number, height: number }
setSize: (size: {width: number, height: number}) => void,
isDrawingArea: boolean,
setIsDrawingArea: (value: boolean) => void,
startingContextConnection: StartingContextConnection | null,
setStartingContextConnection: (value: StartingContextConnection | null) => void,
}

View File

@ -1,29 +1,16 @@
'use client'
import dynamic from 'next/dynamic'
import React, { useEffect, useRef, useState } from 'react'
import { useProject, } from '../../context/Project/provider'
import { MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon } from '@heroicons/react/24/outline'
import LanguageSelect from '../utils/LanguageSelect'
import { entities } from '../../wailsjs/wailsjs/go/models'
import React, { useEffect, useRef } from 'react'
import ToolingOverlay from './ToolingOverlay'
import { useStage } from './context/provider'
const CanvasStage = dynamic(() => import('./CanvasStage'), {
ssr: false,
})
const zoomStep = 0.01
const maxZoomLevel = 4
const CanvasStage = dynamic(() => import('./CanvasStage'), { ssr: false })
const DocumentCanvas = () => {
const { getSelectedDocument, selectedAreaId, } = useProject()
const selectedDocument = getSelectedDocument()
const [ selectedArea, setSelectedArea ] = useState<entities.Area | undefined>()
const [zoomLevel, setZoomLevel] = useState(1)
const [size, setSize] = useState({ width: 0, height: 0 })
const { setSize } = useStage()
const thisRef = useRef<HTMLDivElement>(null)
const handleWindowResize = () => {
const width = thisRef?.current?.clientWidth || 0
const height = thisRef?.current?.clientHeight || 0
@ -36,33 +23,10 @@ const DocumentCanvas = () => {
return () => window.removeEventListener('resize', handleWindowResize)
}, [thisRef?.current?.clientWidth, thisRef?.current?.clientHeight])
useEffect(() => {
setSelectedArea(selectedDocument?.areas.find(a => a.id == selectedAreaId))
}, [selectedAreaId])
return <div ref={thisRef} className='relative' style={{ height: 'calc(100vh - 140px)' }}>
<div className='h-full overflow-hidden rounded-lg border-4 border-dashed border-gray-200'>
<CanvasStage size={size} scale={zoomLevel} scaleStep={zoomStep} setScale={setZoomLevel} maxScale={maxZoomLevel} />
<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>
<LanguageSelect styles={{fontSize: '16px', borderRadius: '2px'}} defaultLanguage={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={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>
<CanvasStage />
<ToolingOverlay />
</div>
</div >
}

View File

@ -6,6 +6,8 @@ export type RectangleCoordinates = {
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 SetZoomCallback = (newZoomLevel: number) => void

View File

@ -4,6 +4,7 @@ import { useNavigation } from '../../context/Navigation/provider'
import { workspaces } from '../../context/Navigation/types'
import { useProject } from '../../context/Project/provider'
import DocumentCanvas from '../DocumentCanvas'
import { StageProvider } from '../DocumentCanvas/context/provider'
import NoSelectedDocument from './NoSelectedDocument'
import TextEditor from './TextEditor'
@ -11,10 +12,14 @@ const MainWorkspace = () => {
const { getSelectedDocument, selectedDocumentId } = useProject()
const { selectedWorkspace } = useNavigation()
const renderSelectedWorkSpace = () => {
const renderSelectedWorkSpace = () => {
if (selectedWorkspace === workspaces.TEXTEDITOR) return <TextEditor />
else return !selectedDocumentId ? <NoSelectedDocument /> : <DocumentCanvas />
}
else return !selectedDocumentId
? <NoSelectedDocument />
: <StageProvider>
<DocumentCanvas />
</StageProvider>
}
return <main className=" bg-gray-100 min-h-[calc(100vh-118px)] ml-64 overflow-y-scroll">
<div className='flex-1'>
@ -26,7 +31,7 @@ const renderSelectedWorkSpace = () => {
Image Processor
</h1> : ''}
</div>
{ renderSelectedWorkSpace() }
{renderSelectedWorkSpace()}
</div>
</div>
</div>

View File

@ -135,7 +135,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
props.document.id === selectedDocumentId
? 'bg-gray-900 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}
@ -143,7 +143,7 @@ const DocumentLineItem = (props: { document: SidebarDocument, groupId: string, i
}
<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)} />
</summary>
<ul>

View File

@ -51,6 +51,7 @@ const processImageArea = async (documentId: string, areaId: string) => {
lines: result.data.lines.map((l: any) => new entities.ProcessedLine({
fullText: l.text,
words: l.words.map((w: any) => new entities.ProcessedWord({
areaId: foundArea.id,
fullText: w.text,
direction: w.direction,
confidence: w.confidence,

View File

@ -43,6 +43,8 @@ export function RequestChangeSessionProjectByName(arg1:string):Promise<boolean>;
export function RequestChooseUserAvatar():Promise<string>;
export function RequestConnectAreaAsTailToNode(arg1:string,arg2:string):Promise<boolean>;
export function RequestDeleteAreaById(arg1:string):Promise<boolean>;
export function RequestDeleteDocumentAndChildren(arg1:string):Promise<boolean>;
@ -57,6 +59,8 @@ export function RequestSaveLocalUserProcessedMarkdownCollection():Promise<boolea
export function RequestSaveProcessedTextCollection():Promise<boolean>;
export function RequestTranslateArea(arg1:string):Promise<boolean>;
export function RequestUpdateArea(arg1:entities.Area):Promise<boolean>;
export function RequestUpdateCurrentUser(arg1:entities.User):Promise<entities.User>;

View File

@ -82,6 +82,10 @@ export function RequestChooseUserAvatar() {
return window['go']['ipc']['Channel']['RequestChooseUserAvatar']();
}
export function RequestConnectAreaAsTailToNode(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestConnectAreaAsTailToNode'](arg1, arg2);
}
export function RequestDeleteAreaById(arg1) {
return window['go']['ipc']['Channel']['RequestDeleteAreaById'](arg1);
}
@ -110,6 +114,10 @@ export function RequestSaveProcessedTextCollection() {
return window['go']['ipc']['Channel']['RequestSaveProcessedTextCollection']();
}
export function RequestTranslateArea(arg1) {
return window['go']['ipc']['Channel']['RequestTranslateArea'](arg1);
}
export function RequestUpdateArea(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateArea'](arg1);
}

View File

@ -26,6 +26,7 @@ export namespace entities {
endX: number;
endY: number;
language: Language;
translateLanguage: Language;
order: number;
static createFrom(source: any = {}) {
@ -41,6 +42,7 @@ export namespace entities {
this.endX = source["endX"];
this.endY = source["endY"];
this.language = this.convertValues(source["language"], Language);
this.translateLanguage = this.convertValues(source["translateLanguage"], Language);
this.order = source["order"];
}
@ -239,6 +241,7 @@ export namespace entities {
}
export class ProcessedWord {
id: string;
areaId: string;
fullText: string;
symbols: ProcessedSymbol[];
confidence: number;
@ -252,6 +255,7 @@ export namespace entities {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.areaId = source["areaId"];
this.fullText = source["fullText"];
this.symbols = this.convertValues(source["symbols"], ProcessedSymbol);
this.confidence = source["confidence"];

1
go.mod
View File

@ -6,6 +6,7 @@ go 1.18
require (
github.com/google/uuid v1.3.0
github.com/snakesel/libretranslate v0.0.2
github.com/wailsapp/wails/v2 v2.5.1
)

2
go.sum
View File

@ -38,6 +38,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw=
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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=

View File

@ -1,5 +1,10 @@
package ipc
import (
document "textualize/core/Document"
"textualize/translate"
)
type Channel struct{}
var channelInstance *Channel
@ -11,3 +16,34 @@ func GetInstance() *Channel {
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
}
}

22
ipc/ContextGroup.go Normal file
View File

@ -0,0 +1,22 @@
package ipc
import (
document "textualize/core/Document"
"textualize/entities"
)
func (c *Channel) RequestConnectAreaAsTailToNode(tailId string, headId string) bool {
processedAreaOfTail := document.GetProcessedAreaCollection().GetAreaById(tailId)
if processedAreaOfTail == nil {
return false
}
processedAreaOfHead := document.GetProcessedAreaCollection().GetAreaById(headId)
if processedAreaOfHead == nil {
return false
}
entities.GetContextGroupCollection().ConnectAreaAsTailToNode(*processedAreaOfTail, *processedAreaOfHead)
return true
}

View File

@ -10,7 +10,12 @@ import (
)
func (c *Channel) GetProcessedAreaById(id string) entities.ProcessedArea {
return *document.GetProcessedAreaCollection().GetAreaById(id)
foundArea := document.GetProcessedAreaCollection().GetAreaById(id)
if foundArea != nil {
return *foundArea
} else {
return entities.ProcessedArea{}
}
}
func (c *Channel) GetProcessedAreasByDocumentId(id string) []entities.ProcessedArea {

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