Compare commits
No commits in common. "c65509b53f3c2296db6f9db8d02ceb1c11a69df8" and "8252b6fcf031fc160417474fcb0087a4857deb66" have entirely different histories.
c65509b53f
...
8252b6fcf0
19 changed files with 489 additions and 836 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
PUBLIC_API_BASE_URL=http://localhost:8000
|
|
||||||
PRIVATE_JWT_SECRET=changeme
|
|
||||||
PRIVATE_DEEPL_API_KEY=changeme
|
|
||||||
|
|
@ -39,9 +39,5 @@
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.1.0",
|
"vitest": "^4.1.0",
|
||||||
"vitest-browser-svelte": "^2.0.2"
|
"vitest-browser-svelte": "^2.0.2"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"deepl-node": "^1.24.0",
|
|
||||||
"valibot": "^1.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,6 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
|
||||||
deepl-node:
|
|
||||||
specifier: ^1.24.0
|
|
||||||
version: 1.24.0
|
|
||||||
valibot:
|
|
||||||
specifier: ^1.3.1
|
|
||||||
version: 1.3.1(typescript@5.9.3)
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/compat':
|
'@eslint/compat':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
|
|
@ -716,10 +709,6 @@ packages:
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
adm-zip@0.5.16:
|
|
||||||
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
|
||||||
engines: {node: '>=12.0'}
|
|
||||||
|
|
||||||
ajv@6.14.0:
|
ajv@6.14.0:
|
||||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||||
|
|
||||||
|
|
@ -742,12 +731,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
asynckit@0.4.0:
|
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
|
||||||
|
|
||||||
axios@1.14.0:
|
|
||||||
resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
|
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -778,10 +761,6 @@ packages:
|
||||||
magicast:
|
magicast:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
|
||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
callsites@3.1.0:
|
callsites@3.1.0:
|
||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -823,10 +802,6 @@ packages:
|
||||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
|
||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
|
|
||||||
commander@14.0.3:
|
commander@14.0.3:
|
||||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
@ -872,10 +847,6 @@ packages:
|
||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
deepl-node@1.24.0:
|
|
||||||
resolution: {integrity: sha512-vZ9jUpzJRvFamgVOfm1LDy3YYJ7k8FhxtAX9whR92EFshLIP9JlYS0HFwXL5yYsfqzXdb/wssGRSWvR48t7nSg==}
|
|
||||||
engines: {node: '>=12.0'}
|
|
||||||
|
|
||||||
deepmerge@4.3.1:
|
deepmerge@4.3.1:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -895,10 +866,6 @@ packages:
|
||||||
defu@6.1.4:
|
defu@6.1.4:
|
||||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
|
||||||
engines: {node: '>=0.4.0'}
|
|
||||||
|
|
||||||
destr@2.0.5:
|
destr@2.0.5:
|
||||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||||
|
|
||||||
|
|
@ -909,29 +876,9 @@ packages:
|
||||||
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
|
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-errors@1.3.0:
|
|
||||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-module-lexer@2.0.0:
|
es-module-lexer@2.0.0:
|
||||||
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
|
||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
es-set-tostringtag@2.1.0:
|
|
||||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1055,23 +1002,6 @@ packages:
|
||||||
flatted@3.4.2:
|
flatted@3.4.2:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||||
|
|
||||||
follow-redirects@1.15.11:
|
|
||||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
|
||||||
engines: {node: '>=4.0'}
|
|
||||||
peerDependencies:
|
|
||||||
debug: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
debug:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
form-data@3.0.4:
|
|
||||||
resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
form-data@4.0.5:
|
|
||||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
fsevents@2.3.2:
|
fsevents@2.3.2:
|
||||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
|
@ -1085,14 +1015,6 @@ packages:
|
||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
get-proto@1.0.1:
|
|
||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
get-tsconfig@4.13.6:
|
get-tsconfig@4.13.6:
|
||||||
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||||
|
|
||||||
|
|
@ -1116,22 +1038,10 @@ packages:
|
||||||
resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==}
|
resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
gopd@1.2.0:
|
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
has-flag@4.0.0:
|
has-flag@4.0.0:
|
||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
has-symbols@1.1.0:
|
|
||||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
|
||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -1239,25 +1149,9 @@ packages:
|
||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
loglevel@1.9.2:
|
|
||||||
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
|
|
||||||
mime-db@1.52.0:
|
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
minimatch@10.2.4:
|
minimatch@10.2.4:
|
||||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
@ -1410,10 +1304,6 @@ packages:
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
proxy-from-env@2.1.0:
|
|
||||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -1575,18 +1465,6 @@ packages:
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
uuid@8.3.2:
|
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
valibot@1.3.1:
|
|
||||||
resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=5'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
vite@7.3.1:
|
vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
@ -2291,8 +2169,6 @@ snapshots:
|
||||||
|
|
||||||
acorn@8.16.0: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
adm-zip@0.5.16: {}
|
|
||||||
|
|
||||||
ajv@6.14.0:
|
ajv@6.14.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
|
|
@ -2312,16 +2188,6 @@ snapshots:
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
|
||||||
|
|
||||||
axios@1.14.0:
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.15.11
|
|
||||||
form-data: 4.0.5
|
|
||||||
proxy-from-env: 2.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
@ -2356,11 +2222,6 @@ snapshots:
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
rc9: 2.1.2
|
rc9: 2.1.2
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
function-bind: 1.1.2
|
|
||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
chai@6.2.2: {}
|
chai@6.2.2: {}
|
||||||
|
|
@ -2394,10 +2255,6 @@ snapshots:
|
||||||
|
|
||||||
color-support@1.1.3: {}
|
color-support@1.1.3: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
|
||||||
dependencies:
|
|
||||||
delayed-stream: 1.0.0
|
|
||||||
|
|
||||||
commander@14.0.3: {}
|
commander@14.0.3: {}
|
||||||
|
|
||||||
commondir@1.0.1: {}
|
commondir@1.0.1: {}
|
||||||
|
|
@ -2426,17 +2283,6 @@ snapshots:
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
deepl-node@1.24.0:
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 22.19.15
|
|
||||||
adm-zip: 0.5.16
|
|
||||||
axios: 1.14.0
|
|
||||||
form-data: 3.0.4
|
|
||||||
loglevel: 1.9.2
|
|
||||||
uuid: 8.3.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@5.0.1: {}
|
default-browser-id@5.0.1: {}
|
||||||
|
|
@ -2450,37 +2296,14 @@ snapshots:
|
||||||
|
|
||||||
defu@6.1.4: {}
|
defu@6.1.4: {}
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
|
||||||
|
|
||||||
destr@2.0.5: {}
|
destr@2.0.5: {}
|
||||||
|
|
||||||
devalue@5.6.4: {}
|
devalue@5.6.4: {}
|
||||||
|
|
||||||
dotenv@17.3.1: {}
|
dotenv@17.3.1: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers: 1.0.2
|
|
||||||
es-errors: 1.3.0
|
|
||||||
gopd: 1.2.0
|
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
|
||||||
|
|
||||||
es-module-lexer@2.0.0: {}
|
es-module-lexer@2.0.0: {}
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
|
|
||||||
es-set-tostringtag@2.1.0:
|
|
||||||
dependencies:
|
|
||||||
es-errors: 1.3.0
|
|
||||||
get-intrinsic: 1.3.0
|
|
||||||
has-tostringtag: 1.0.2
|
|
||||||
hasown: 2.0.2
|
|
||||||
|
|
||||||
esbuild@0.27.4:
|
esbuild@0.27.4:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.4
|
'@esbuild/aix-ppc64': 0.27.4
|
||||||
|
|
@ -2647,24 +2470,6 @@ snapshots:
|
||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.2: {}
|
||||||
|
|
||||||
follow-redirects@1.15.11: {}
|
|
||||||
|
|
||||||
form-data@3.0.4:
|
|
||||||
dependencies:
|
|
||||||
asynckit: 0.4.0
|
|
||||||
combined-stream: 1.0.8
|
|
||||||
es-set-tostringtag: 2.1.0
|
|
||||||
hasown: 2.0.2
|
|
||||||
mime-types: 2.1.35
|
|
||||||
|
|
||||||
form-data@4.0.5:
|
|
||||||
dependencies:
|
|
||||||
asynckit: 0.4.0
|
|
||||||
combined-stream: 1.0.8
|
|
||||||
es-set-tostringtag: 2.1.0
|
|
||||||
hasown: 2.0.2
|
|
||||||
mime-types: 2.1.35
|
|
||||||
|
|
||||||
fsevents@2.3.2:
|
fsevents@2.3.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -2673,24 +2478,6 @@ snapshots:
|
||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
call-bind-apply-helpers: 1.0.2
|
|
||||||
es-define-property: 1.0.1
|
|
||||||
es-errors: 1.3.0
|
|
||||||
es-object-atoms: 1.1.1
|
|
||||||
function-bind: 1.1.2
|
|
||||||
get-proto: 1.0.1
|
|
||||||
gopd: 1.2.0
|
|
||||||
has-symbols: 1.1.0
|
|
||||||
hasown: 2.0.2
|
|
||||||
math-intrinsics: 1.1.0
|
|
||||||
|
|
||||||
get-proto@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
dunder-proto: 1.0.1
|
|
||||||
es-object-atoms: 1.1.1
|
|
||||||
|
|
||||||
get-tsconfig@4.13.6:
|
get-tsconfig@4.13.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
@ -2714,16 +2501,8 @@ snapshots:
|
||||||
|
|
||||||
globals@17.4.0: {}
|
globals@17.4.0: {}
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
|
||||||
|
|
||||||
has-tostringtag@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
has-symbols: 1.1.0
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
@ -2808,20 +2587,10 @@ snapshots:
|
||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
loglevel@1.9.2: {}
|
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
|
||||||
|
|
||||||
mime-db@1.52.0: {}
|
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
dependencies:
|
|
||||||
mime-db: 1.52.0
|
|
||||||
|
|
||||||
minimatch@10.2.4:
|
minimatch@10.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.4
|
brace-expansion: 5.0.4
|
||||||
|
|
@ -2949,8 +2718,6 @@ snapshots:
|
||||||
|
|
||||||
prettier@3.8.1: {}
|
prettier@3.8.1: {}
|
||||||
|
|
||||||
proxy-from-env@2.1.0: {}
|
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
rc9@2.1.2:
|
rc9@2.1.2:
|
||||||
|
|
@ -3128,12 +2895,6 @@ snapshots:
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
uuid@8.3.2: {}
|
|
||||||
|
|
||||||
valibot@1.3.1(typescript@5.9.3):
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1):
|
vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
# Makes a request to localhost:8000/openapi.json and saves the result in ./src/lib/openapi.json
|
# Makes a request to localhost:8000/openapi.json and saves the result in ./src/lib/openapi.json
|
||||||
curl -o ./src/lib/openapi.json http://localhost:8000/openapi.json
|
curl -o ./src/lib/openapi.json http://localhost:8000/openapi.json
|
||||||
pnpm openapi-ts:gen
|
|
||||||
|
|
@ -28,12 +28,6 @@ export type ArticleDetail = {
|
||||||
* Source Body
|
* Source Body
|
||||||
*/
|
*/
|
||||||
source_body: string;
|
source_body: string;
|
||||||
/**
|
|
||||||
* Source Body Pos
|
|
||||||
*/
|
|
||||||
source_body_pos: {
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Target Language
|
* Target Language
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,7 +53,7 @@ export type ArticleDetail = {
|
||||||
*/
|
*/
|
||||||
target_body_pos: {
|
target_body_pos: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
} | null;
|
||||||
/**
|
/**
|
||||||
* Target Body Transcript
|
* Target Body Transcript
|
||||||
*/
|
*/
|
||||||
|
|
@ -177,9 +171,17 @@ export type JobResponse = {
|
||||||
*/
|
*/
|
||||||
status: string;
|
status: string;
|
||||||
/**
|
/**
|
||||||
* Translated Article Id
|
* Source Language
|
||||||
*/
|
*/
|
||||||
translated_article_id?: string | null;
|
source_language: string;
|
||||||
|
/**
|
||||||
|
* Target Language
|
||||||
|
*/
|
||||||
|
target_language: string;
|
||||||
|
/**
|
||||||
|
* Complexity Level
|
||||||
|
*/
|
||||||
|
complexity_level: string;
|
||||||
/**
|
/**
|
||||||
* Created At
|
* Created At
|
||||||
*/
|
*/
|
||||||
|
|
@ -192,6 +194,40 @@ export type JobResponse = {
|
||||||
* Completed At
|
* Completed At
|
||||||
*/
|
*/
|
||||||
completed_at?: string | null;
|
completed_at?: string | null;
|
||||||
|
/**
|
||||||
|
* Generated Text
|
||||||
|
*/
|
||||||
|
generated_text?: string | null;
|
||||||
|
/**
|
||||||
|
* Generated Text Pos
|
||||||
|
*/
|
||||||
|
generated_text_pos?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Translated Text
|
||||||
|
*/
|
||||||
|
translated_text?: string | null;
|
||||||
|
/**
|
||||||
|
* Translated Text Pos
|
||||||
|
*/
|
||||||
|
translated_text_pos?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Input Summary
|
||||||
|
*/
|
||||||
|
input_summary?: string | null;
|
||||||
|
/**
|
||||||
|
* Audio Url
|
||||||
|
*/
|
||||||
|
audio_url?: string | null;
|
||||||
|
/**
|
||||||
|
* Audio Transcript
|
||||||
|
*/
|
||||||
|
audio_transcript?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
/**
|
/**
|
||||||
* Error Message
|
* Error Message
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
const languageNames: Record<string, string> = {
|
|
||||||
en: 'English',
|
|
||||||
fr: 'French',
|
|
||||||
es: 'Spanish',
|
|
||||||
it: 'Italian',
|
|
||||||
de: 'German'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatLanguage = (code: string) => languageNames[code] ?? code.toUpperCase();
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { formatLanguage } from './formatLanguage';
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,19 +0,0 @@
|
||||||
export interface PartsOfSpeechData {
|
|
||||||
language: string;
|
|
||||||
sentences: {
|
|
||||||
text: string;
|
|
||||||
tokens: PartOfSpeechToken[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PartOfSpeechToken {
|
|
||||||
dep: string;
|
|
||||||
pos: string;
|
|
||||||
tag: string;
|
|
||||||
text: string;
|
|
||||||
type: null | string;
|
|
||||||
lemma: string;
|
|
||||||
is_stop: boolean;
|
|
||||||
is_alpha: boolean;
|
|
||||||
is_punct: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { getArticleBffArticlesArticleIdGet } from '../../../../client/sdk.gen.ts';
|
import { getArticleBffArticlesArticleIdGet } from '../../../../client/sdk.gen.ts';
|
||||||
|
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
const { data, response } = await getArticleBffArticlesArticleIdGet({
|
const { data, response } = await getArticleBffArticlesArticleIdGet({
|
||||||
|
|
@ -12,5 +13,9 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
error(response.status === 404 ? 404 : 500, 'Article not found');
|
error(response.status === 404 ? 404 : 500, 'Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { article: data };
|
const audioUrl = data.target_audio_url
|
||||||
|
? `${PUBLIC_API_BASE_URL}/media/${data.target_audio_url}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { article: data, audioUrl };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,81 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import type { PartsOfSpeechData } from '$lib/spacy/types';
|
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import TargetLanguageBody from './TargetLanguageBody.svelte';
|
|
||||||
import type { Paragraph, Sentence, SentenceToken, Transcript } from './Transcript';
|
|
||||||
import TranslationPanel from './TranslationPanel.svelte';
|
|
||||||
import { translateText } from './translate.remote';
|
|
||||||
|
|
||||||
const { data }: PageProps = $props();
|
const { data }: PageProps = $props();
|
||||||
const { article } = data;
|
const { article, audioUrl } = data;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Body parsing: split into paragraphs → sentences → tokens
|
// Body parsing: split into paragraphs → sentences → tokens
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
function extractParagraphsAndWordCount(text: PartsOfSpeechData): {
|
type WordToken = { type: 'word'; text: string; wordIdx: number };
|
||||||
paragraphs: Paragraph[];
|
type OtherToken = { type: 'other'; text: string };
|
||||||
totalWords: number;
|
type Token = WordToken | OtherToken;
|
||||||
} {
|
|
||||||
const paragraphs: Paragraph[] = [{ index: 0, sentences: [] }];
|
type Sentence = {
|
||||||
|
tokens: Token[];
|
||||||
|
idx: number; // global sentence index
|
||||||
|
startWordIdx: number;
|
||||||
|
endWordIdx: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Paragraph = { sentences: Sentence[] };
|
||||||
|
|
||||||
|
function parseBody(text: string): { paragraphs: Paragraph[]; totalWords: number } {
|
||||||
|
const paragraphs: Paragraph[] = [];
|
||||||
let wordIdx = 0;
|
let wordIdx = 0;
|
||||||
let sentenceIdx = 0;
|
let sentenceIdx = 0;
|
||||||
|
|
||||||
text.sentences.forEach((s) => {
|
for (const paraText of text.split(/\n\n+/)) {
|
||||||
const sentence: Sentence = {
|
if (!paraText.trim()) continue;
|
||||||
idx: sentenceIdx++,
|
|
||||||
text: s.text,
|
|
||||||
startWordIdx: wordIdx,
|
|
||||||
endWordIdx: wordIdx + s.tokens.length - 1,
|
|
||||||
tokens: s.tokens.map((t) => ({
|
|
||||||
...t,
|
|
||||||
idx: wordIdx++
|
|
||||||
})) as SentenceToken[]
|
|
||||||
};
|
|
||||||
|
|
||||||
const sentenceEndsWithNewLine = s.text.endsWith('\n');
|
// Split into alternating word / non-word tokens
|
||||||
if (sentenceEndsWithNewLine) {
|
const rawTokens = paraText.match(/[\p{L}\p{N}\u2019'''-]+|[^\p{L}\p{N}\u2019'''-]+/gu) ?? [];
|
||||||
paragraphs.push({ index: paragraphs.length, sentences: [] });
|
|
||||||
} else {
|
const sentences: Sentence[] = [];
|
||||||
paragraphs[paragraphs.length - 1].sentences.push(sentence);
|
let currentTokens: Token[] = [];
|
||||||
|
let startWordIdx = wordIdx;
|
||||||
|
let hasWord = false;
|
||||||
|
|
||||||
|
for (const raw of rawTokens) {
|
||||||
|
if (/[\p{L}\p{N}]/u.test(raw)) {
|
||||||
|
currentTokens.push({ type: 'word', text: raw, wordIdx: wordIdx++ });
|
||||||
|
hasWord = true;
|
||||||
|
} else {
|
||||||
|
currentTokens.push({ type: 'other', text: raw });
|
||||||
|
// Flush sentence on sentence-ending punctuation
|
||||||
|
if (hasWord && /[.!?]/.test(raw)) {
|
||||||
|
sentences.push({
|
||||||
|
tokens: [...currentTokens],
|
||||||
|
idx: sentenceIdx++,
|
||||||
|
startWordIdx,
|
||||||
|
endWordIdx: wordIdx - 1
|
||||||
|
});
|
||||||
|
currentTokens = [];
|
||||||
|
startWordIdx = wordIdx;
|
||||||
|
hasWord = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (currentTokens.length > 0) {
|
||||||
|
sentences.push({
|
||||||
|
tokens: currentTokens,
|
||||||
|
idx: sentenceIdx++,
|
||||||
|
startWordIdx,
|
||||||
|
endWordIdx: wordIdx - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentences.length > 0) {
|
||||||
|
paragraphs.push({ sentences });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { paragraphs, totalWords: wordIdx };
|
return { paragraphs, totalWords: wordIdx };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { paragraphs } = extractParagraphsAndWordCount(
|
const { paragraphs } = parseBody(article.target_body);
|
||||||
article.target_body_pos as Record<string, any> as PartsOfSpeechData
|
|
||||||
);
|
|
||||||
|
|
||||||
// Flat sentence list for O(n) audio-time lookup
|
// Flat sentence list for O(n) audio-time lookup
|
||||||
const allSentences: Array<{ idx: number; startWordIdx: number; endWordIdx: number }> = [];
|
const allSentences: Array<{ idx: number; startWordIdx: number; endWordIdx: number }> = [];
|
||||||
|
|
@ -63,24 +91,18 @@
|
||||||
|
|
||||||
type WordTiming = { start: number; end: number };
|
type WordTiming = { start: number; end: number };
|
||||||
|
|
||||||
function extractWordTimings(transcript: Transcript | null): WordTiming[] {
|
function extractWordTimings(transcript: Record<string, unknown> | null): WordTiming[] {
|
||||||
if (!transcript) return [];
|
if (!transcript) return [];
|
||||||
try {
|
try {
|
||||||
const timings: WordTiming[] = [];
|
const words = (transcript as any)?.results?.channels?.[0]?.alternatives?.[0]?.words;
|
||||||
for (const utterance of transcript.utterances) {
|
if (!Array.isArray(words)) return [];
|
||||||
for (const word of utterance.words) {
|
return words.map((w: any) => ({ start: Number(w.start), end: Number(w.end) }));
|
||||||
timings.push({ start: word.start, end: word.end });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return timings;
|
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wordTimings = extractWordTimings(
|
const wordTimings = extractWordTimings(article.target_body_transcript);
|
||||||
article.target_body_transcript as unknown as Transcript | null
|
|
||||||
);
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Reactive state
|
// Reactive state
|
||||||
|
|
@ -88,8 +110,7 @@
|
||||||
|
|
||||||
let audioEl: HTMLAudioElement | null = $state(null);
|
let audioEl: HTMLAudioElement | null = $state(null);
|
||||||
let activeSentenceIdx = $state(-1);
|
let activeSentenceIdx = $state(-1);
|
||||||
let selectedSentenceToken: SentenceToken | null = $state(null);
|
let selectedWord: WordToken | null = $state(null);
|
||||||
let selectedSentence: Sentence | null = $state(null);
|
|
||||||
let translatedText: string | null = $state(null);
|
let translatedText: string | null = $state(null);
|
||||||
let translating = $state(false);
|
let translating = $state(false);
|
||||||
|
|
||||||
|
|
@ -128,22 +149,21 @@
|
||||||
// Word click: fetch translation
|
// Word click: fetch translation
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
async function handleWordClick(token: SentenceToken, sentence: Sentence) {
|
async function handleWordClick(token: WordToken) {
|
||||||
selectedSentenceToken = token;
|
selectedWord = token;
|
||||||
activeSentenceIdx = sentence.idx;
|
|
||||||
translatedText = null;
|
translatedText = null;
|
||||||
translating = true;
|
translating = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await translateText({
|
const params = new URLSearchParams({
|
||||||
fromLanguage: article.target_language,
|
text: token.text,
|
||||||
toLanguage: article.source_language,
|
target_language: article.source_language
|
||||||
sentenceText: sentence.text,
|
|
||||||
text: token.text
|
|
||||||
});
|
});
|
||||||
|
const res = await fetch(`/app/translate?${params}`);
|
||||||
translatedText = result.text;
|
if (res.ok) {
|
||||||
console.log({ result });
|
const body = await res.json();
|
||||||
|
translatedText = body.translated_text ?? null;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
translatedText = null;
|
translatedText = null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -152,7 +172,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePanel() {
|
function closePanel() {
|
||||||
selectedSentenceToken = null;
|
selectedWord = null;
|
||||||
translatedText = null;
|
translatedText = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +207,7 @@
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<nav class="breadcrumb">
|
<nav class="breadcrumb">
|
||||||
<a href={resolve('/app/articles')} class="link">← Articles</a>
|
<a href="/app/articles" class="link">← Articles</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header class="article-header">
|
<header class="article-header">
|
||||||
|
|
@ -198,11 +218,11 @@
|
||||||
<div class="article-layout">
|
<div class="article-layout">
|
||||||
<!-- Main content: audio + body -->
|
<!-- Main content: audio + body -->
|
||||||
<div class="article-main">
|
<div class="article-main">
|
||||||
{#if article.target_audio_url}
|
{#if audioUrl}
|
||||||
<div class="audio-section">
|
<div class="audio-section">
|
||||||
<audio
|
<audio
|
||||||
bind:this={audioEl}
|
bind:this={audioEl}
|
||||||
src={article.target_audio_url}
|
src={audioUrl}
|
||||||
controls
|
controls
|
||||||
ontimeupdate={handleTimeUpdate}
|
ontimeupdate={handleTimeUpdate}
|
||||||
class="audio-player"
|
class="audio-player"
|
||||||
|
|
@ -212,16 +232,54 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<TargetLanguageBody
|
<div class="article-body" lang={article.target_language}>
|
||||||
lang={article.source_language}
|
{#each paragraphs as para}
|
||||||
{paragraphs}
|
<p class="paragraph">
|
||||||
{activeSentenceIdx}
|
{#each para.sentences as sentence}<span
|
||||||
onWordClick={handleWordClick}
|
class="sentence"
|
||||||
{selectedSentenceToken}
|
class:sentence--active={activeSentenceIdx === sentence.idx}
|
||||||
/>
|
>{#each sentence.tokens as token}{#if token.type === 'word'}<button
|
||||||
|
class="word"
|
||||||
|
class:word--selected={selectedWord?.wordIdx === token.wordIdx}
|
||||||
|
onclick={() => handleWordClick(token)}>{token.text}</button
|
||||||
|
>{:else}{token.text}{/if}{/each}</span
|
||||||
|
>{/each}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TranslationPanel {closePanel} {selectedSentenceToken} {translatedText} {translating} />
|
<!-- Translation panel (desktop: sticky sidebar; mobile: bottom drawer) -->
|
||||||
|
<aside
|
||||||
|
class="translation-panel"
|
||||||
|
class:is-open={selectedWord !== null}
|
||||||
|
aria-label="Word translation"
|
||||||
|
>
|
||||||
|
{#if selectedWord}
|
||||||
|
<div class="panel-header">
|
||||||
|
<p class="panel-word">{selectedWord.text}</p>
|
||||||
|
<button class="btn btn-ghost panel-close" onclick={closePanel} aria-label="Close panel">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if translating}
|
||||||
|
<div class="panel-loading">
|
||||||
|
<div class="spinner" aria-hidden="true"></div>
|
||||||
|
<span>Translating…</span>
|
||||||
|
</div>
|
||||||
|
{:else if translatedText}
|
||||||
|
<p class="panel-translation">{translatedText}</p>
|
||||||
|
<button class="btn btn-secondary panel-save" disabled aria-disabled="true">
|
||||||
|
Add to flashcard
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<p class="panel-error">Could not load translation.</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="panel-hint">Tap any word for a translation</p>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -229,7 +287,7 @@
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="drawer-backdrop"
|
class="drawer-backdrop"
|
||||||
class:is-visible={selectedSentenceToken !== null}
|
class:is-visible={selectedWord !== null}
|
||||||
onclick={closePanel}
|
onclick={closePanel}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
></div>
|
||||||
|
|
@ -323,7 +381,69 @@
|
||||||
accent-color: var(--color-primary);
|
accent-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Article body --- */
|
||||||
|
.article-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-body-xl);
|
||||||
|
line-height: 2;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sentence: highlighted when audio is at that point */
|
||||||
|
.sentence {
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
transition: background-color var(--duration-normal) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sentence--active {
|
||||||
|
background-color: var(--color-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Word buttons --- */
|
||||||
|
.word {
|
||||||
|
display: inline;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0 0.05em;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
transition:
|
||||||
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
|
color var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word--selected {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Translation panel: Desktop (sticky sidebar) --- */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
.translation-panel {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--space-6);
|
||||||
|
background-color: var(--color-surface-container-lowest);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-5);
|
||||||
|
min-height: 16rem;
|
||||||
|
box-shadow: var(--shadow-tonal-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-backdrop {
|
.drawer-backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +451,26 @@
|
||||||
|
|
||||||
/* --- Translation panel: Mobile (bottom drawer) --- */
|
/* --- Translation panel: Mobile (bottom drawer) --- */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.translation-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 300;
|
||||||
|
background-color: var(--color-surface-container-lowest);
|
||||||
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||||
|
padding: var(--space-5) var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom));
|
||||||
|
max-height: 55vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform var(--duration-slow) var(--ease-standard);
|
||||||
|
box-shadow: 0 -8px 32px color-mix(in srgb, var(--color-on-surface) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.translation-panel.is-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-backdrop {
|
.drawer-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
@ -347,6 +487,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Panel internals --- */
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-word {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-headline-md);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-translation {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-save {
|
||||||
|
width: 100%;
|
||||||
|
padding-block: var(--space-2);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-hint {
|
||||||
|
font-family: var(--font-label);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-error {
|
||||||
|
font-family: var(--font-label);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
font-family: var(--font-label);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 2px solid var(--color-outline-variant);
|
||||||
|
border-top-color: var(--color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { Paragraph, Sentence, SentenceToken } from './Transcript';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
paragraphs: Paragraph[];
|
|
||||||
activeSentenceIdx: number;
|
|
||||||
selectedSentenceToken: SentenceToken | null;
|
|
||||||
onWordClick: (token: SentenceToken, sentence: Sentence) => void;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { paragraphs, activeSentenceIdx, selectedSentenceToken, onWordClick, lang }: Props =
|
|
||||||
$props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="article-body" {lang}>
|
|
||||||
{#each paragraphs as para (para.index)}
|
|
||||||
<p class="paragraph">
|
|
||||||
{#each para.sentences as sentence (sentence.idx)}
|
|
||||||
<span class="sentence" class:sentence--active={activeSentenceIdx === sentence.idx}>
|
|
||||||
{#each sentence.tokens as token (token.idx)}
|
|
||||||
{#if !token.is_punct}
|
|
||||||
<button
|
|
||||||
class="word"
|
|
||||||
class:word--selected={selectedSentenceToken?.idx === token.idx}
|
|
||||||
onclick={() => onWordClick(token, sentence)}
|
|
||||||
>
|
|
||||||
{token.text}
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
{token.text}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</p>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.article-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-4);
|
|
||||||
font-family: var(--font-body);
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: var(--text-body-xl);
|
|
||||||
line-height: 2;
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sentence: highlighted when audio is at that point */
|
|
||||||
.sentence {
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
transition: background-color var(--duration-normal) var(--ease-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sentence--active {
|
|
||||||
background-color: var(--color-primary-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Word buttons --- */
|
|
||||||
.word {
|
|
||||||
display: inline;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0 0.1em;
|
|
||||||
margin: 0;
|
|
||||||
font: inherit;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
transition:
|
|
||||||
background-color var(--duration-fast) var(--ease-standard),
|
|
||||||
color var(--duration-fast) var(--ease-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
.word:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.word--selected {
|
|
||||||
background-color: color-mix(in srgb, var(--color-primary) 20%, transparent);
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-weight: var(--weight-medium);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import type { PartOfSpeechToken } from '$lib/spacy/types';
|
|
||||||
|
|
||||||
export interface Transcript {
|
|
||||||
channels: {
|
|
||||||
alternatives: {
|
|
||||||
end: number;
|
|
||||||
word: string;
|
|
||||||
start: number;
|
|
||||||
confidence: number;
|
|
||||||
punctuated_word: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
utterances: {
|
|
||||||
id: string;
|
|
||||||
transcript: string;
|
|
||||||
confidence: number;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
words: {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
word: string;
|
|
||||||
confidence: number;
|
|
||||||
punctuated_word: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SentenceToken extends PartOfSpeechToken {
|
|
||||||
idx: number; // index of the token in the entire article
|
|
||||||
}
|
|
||||||
export type Sentence = {
|
|
||||||
tokens: SentenceToken[];
|
|
||||||
text: string;
|
|
||||||
idx: number; // global sentence index
|
|
||||||
startWordIdx: number;
|
|
||||||
endWordIdx: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Paragraph = { index: number; sentences: Sentence[] };
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { SentenceToken } from './Transcript';
|
|
||||||
|
|
||||||
let {
|
|
||||||
selectedSentenceToken = null,
|
|
||||||
translating = false,
|
|
||||||
translatedText = '',
|
|
||||||
closePanel
|
|
||||||
}: {
|
|
||||||
selectedSentenceToken?: SentenceToken | null;
|
|
||||||
translating?: boolean;
|
|
||||||
translatedText: string | null;
|
|
||||||
closePanel: () => void;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Translation panel (desktop: sticky sidebar; mobile: bottom drawer) -->
|
|
||||||
<aside
|
|
||||||
class="translation-panel"
|
|
||||||
class:is-open={selectedSentenceToken !== null}
|
|
||||||
aria-label="Word translation"
|
|
||||||
>
|
|
||||||
{#if selectedSentenceToken}
|
|
||||||
<div class="panel-header">
|
|
||||||
<p class="panel-word">{selectedSentenceToken.text}</p>
|
|
||||||
<button class="btn btn-ghost panel-close" onclick={closePanel} aria-label="Close panel">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if translating}
|
|
||||||
<div class="panel-loading">
|
|
||||||
<div class="spinner" aria-hidden="true"></div>
|
|
||||||
<span>Translating…</span>
|
|
||||||
</div>
|
|
||||||
{:else if translatedText}
|
|
||||||
<p class="panel-translation">{translatedText}</p>
|
|
||||||
<button class="btn btn-secondary panel-save" disabled aria-disabled="true">
|
|
||||||
Add to flashcard
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<p class="panel-error">Could not load translation.</p>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<p class="panel-hint">Tap any word for a translation</p>
|
|
||||||
{/if}
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-word {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-headline-md);
|
|
||||||
font-weight: var(--weight-semibold);
|
|
||||||
line-height: var(--leading-snug);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-close {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
font-size: var(--text-body-lg);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-translation {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: var(--text-body-lg);
|
|
||||||
line-height: var(--leading-relaxed);
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
font-style: italic;
|
|
||||||
margin-bottom: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-save {
|
|
||||||
width: 100%;
|
|
||||||
padding-block: var(--space-2);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-hint {
|
|
||||||
font-family: var(--font-label);
|
|
||||||
font-size: var(--text-body-sm);
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-4) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-error {
|
|
||||||
font-family: var(--font-label);
|
|
||||||
font-size: var(--text-body-sm);
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-3);
|
|
||||||
font-family: var(--font-label);
|
|
||||||
font-size: var(--text-body-sm);
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 2px solid var(--color-outline-variant);
|
|
||||||
border-top-color: var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.9s linear infinite;
|
|
||||||
}
|
|
||||||
/* --- Translation panel: Desktop (sticky sidebar) --- */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.translation-panel {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--space-6);
|
|
||||||
background-color: var(--color-surface-container-lowest);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
padding: var(--space-5);
|
|
||||||
min-height: 16rem;
|
|
||||||
box-shadow: var(--shadow-tonal-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Translation panel: Mobile (bottom drawer) --- */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.translation-panel {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 300;
|
|
||||||
background-color: var(--color-surface-container-lowest);
|
|
||||||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
|
||||||
padding: var(--space-5) var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom));
|
|
||||||
max-height: 55vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
transform: translateY(100%);
|
|
||||||
transition: transform var(--duration-slow) var(--ease-standard);
|
|
||||||
box-shadow: 0 -8px 32px color-mix(in srgb, var(--color-on-surface) 8%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.translation-panel.is-open {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { query } from '$app/server';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import * as deepl from 'deepl-node';
|
|
||||||
import { PRIVATE_DEEPL_API_KEY } from '$env/static/private';
|
|
||||||
|
|
||||||
const deeplClient = new deepl.DeepLClient(PRIVATE_DEEPL_API_KEY);
|
|
||||||
|
|
||||||
export const translateText = query(
|
|
||||||
v.object({
|
|
||||||
text: v.string(),
|
|
||||||
fromLanguage: v.string(),
|
|
||||||
toLanguage: v.string(),
|
|
||||||
sentenceText: v.string()
|
|
||||||
}),
|
|
||||||
async ({ fromLanguage, sentenceText, text, toLanguage }) => {
|
|
||||||
const safeToLanguage = toLanguage === 'en' ? 'en-gb' : toLanguage;
|
|
||||||
return await deeplClient.translateText(
|
|
||||||
text,
|
|
||||||
fromLanguage as deepl.SourceLanguageCode,
|
|
||||||
safeToLanguage as deepl.TargetLanguageCode,
|
|
||||||
{ context: sentenceText }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { error, type ServerLoad } from '@sveltejs/kit';
|
import { error, type ServerLoad } from '@sveltejs/kit';
|
||||||
import { getJobApiJobsJobIdGet, getArticleBffArticlesArticleIdGet } from '../../../../client/sdk.gen.ts';
|
import { getJobApiJobsJobIdGet } from '../../../../client/sdk.gen.ts';
|
||||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ params, locals }) => {
|
export const load: ServerLoad = async ({ params, locals }) => {
|
||||||
|
|
@ -8,23 +8,10 @@ export const load: ServerLoad = async ({ params, locals }) => {
|
||||||
path: { job_id: params.job_id as string }
|
path: { job_id: params.job_id as string }
|
||||||
});
|
});
|
||||||
|
|
||||||
let translatedArticle = null;
|
|
||||||
|
|
||||||
if (!data || response.status !== 200) {
|
if (!data || response.status !== 200) {
|
||||||
error(response.status === 404 ? 404 : 500, 'Job not found');
|
error(response.status === 404 ? 404 : 500, 'Job not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.translated_article_id) {
|
|
||||||
const articleResponse = await getArticleBffArticlesArticleIdGet({
|
|
||||||
headers: { Authorization: `Bearer ${locals.authToken ?? ''}` },
|
|
||||||
path: { article_id: data.translated_article_id as string }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (articleResponse.data) {
|
|
||||||
translatedArticle = articleResponse.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullAudioUrl = `${PUBLIC_API_BASE_URL}/media/${data.audio_url}`;
|
const fullAudioUrl = `${PUBLIC_API_BASE_URL}/media/${data.audio_url}`;
|
||||||
return { job: data, fullAudioUrl, translatedArticle };
|
return { job: data, fullAudioUrl };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatLanguage } from '$lib/formatters';
|
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import ArticlePreview from './ArticlePreview.svelte';
|
|
||||||
|
|
||||||
const { data }: PageProps = $props();
|
const { data }: PageProps = $props();
|
||||||
const { job, fullAudioUrl, translatedArticle } = data;
|
const { job } = data;
|
||||||
|
|
||||||
|
const languageNames: Record<string, string> = {
|
||||||
|
en: 'English',
|
||||||
|
fr: 'French',
|
||||||
|
es: 'Spanish',
|
||||||
|
it: 'Italian',
|
||||||
|
de: 'German'
|
||||||
|
};
|
||||||
|
|
||||||
|
const lang = (code: string) => languageNames[code] ?? code.toUpperCase();
|
||||||
|
|
||||||
const fmt = (iso: string | null | undefined) => {
|
const fmt = (iso: string | null | undefined) => {
|
||||||
if (!iso) return null;
|
if (!iso) return null;
|
||||||
|
|
@ -34,6 +42,10 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<dl class="meta-grid">
|
<dl class="meta-grid">
|
||||||
|
<div class="meta-item">
|
||||||
|
<dt class="field-label">Language Pair</dt>
|
||||||
|
<dd class="meta-value">{lang(job.source_language)} → {lang(job.target_language)}</dd>
|
||||||
|
</div>
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<dt class="field-label">Complexity</dt>
|
<dt class="field-label">Complexity</dt>
|
||||||
<dd class="meta-value">{job.complexity_level}</dd>
|
<dd class="meta-value">{job.complexity_level}</dd>
|
||||||
|
|
@ -68,7 +80,62 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ArticlePreview article={translatedArticle} {fullAudioUrl} />
|
{#if job.input_summary}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Input Summary</h2>
|
||||||
|
<div class="prose">{job.input_summary}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if job.generated_text}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">
|
||||||
|
Generated Text
|
||||||
|
<span class="section-lang">{lang(job.target_language)}</span>
|
||||||
|
</h2>
|
||||||
|
{#if job.audio_url}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Audio</h2>
|
||||||
|
<audio class="audio-player" controls src={data.fullAudioUrl}>
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
<div class="prose prose-target">{job.generated_text}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if job.translated_text}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">
|
||||||
|
Translation
|
||||||
|
<span class="section-lang">{lang(job.source_language)}</span>
|
||||||
|
</h2>
|
||||||
|
<div class="prose prose-translated">{job.translated_text}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- POS is JSON data about parts-of-speech -->
|
||||||
|
{#if job.audio_transcript}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Audio Transcript</h2>
|
||||||
|
<div class="pos-text">{JSON.stringify(job.audio_transcript, null, 2)}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if job.generated_text_pos}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Generated Text with POS Tags</h2>
|
||||||
|
<div class="pos-text">{JSON.stringify(job.generated_text_pos, null, 2)}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if job.translated_text_pos}
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Translated Text with POS Tags</h2>
|
||||||
|
<div class="pos-text">{JSON.stringify(job.translated_text_pos, null, 2)}</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -184,6 +251,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Content sections --- */
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--color-outline-variant) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-title-lg);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-lang {
|
||||||
|
font-family: var(--font-label);
|
||||||
|
font-size: var(--text-label-md);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Prose --- */
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose-target {
|
||||||
|
padding: var(--space-5) var(--space-6);
|
||||||
|
background-color: var(--color-surface-container-low);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
font-size: calc(var(--text-body-lg) * 1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose-translated {
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
font-size: var(--text-body-md);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
background-color: var(--color-surface-container-low);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-height: 300px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Audio --- */
|
||||||
|
|
||||||
|
.audio-player {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Responsive --- */
|
/* --- Responsive --- */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { formatLanguage } from '$lib/formatters';
|
|
||||||
import type { GetArticleBffArticlesArticleIdGetResponse } from '../../../../client/types.gen';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
article: GetArticleBffArticlesArticleIdGetResponse | null;
|
|
||||||
fullAudioUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { article, fullAudioUrl }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if article}
|
|
||||||
<section class="content-section">
|
|
||||||
{#if fullAudioUrl}
|
|
||||||
<section class="content-section">
|
|
||||||
<h2 class="section-title">Audio</h2>
|
|
||||||
<audio class="audio-player" controls src={fullAudioUrl}>
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
<h2 class="section-title">
|
|
||||||
Generated Text
|
|
||||||
<span class="section-lang">{formatLanguage(article.target_language)}</span>
|
|
||||||
</h2>
|
|
||||||
<div class="prose prose-target">{article.target_body}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="content-section">
|
|
||||||
<h2 class="section-title">
|
|
||||||
Translation
|
|
||||||
<span class="section-lang">{formatLanguage(article.source_language)}</span>
|
|
||||||
</h2>
|
|
||||||
<div class="prose prose-translated">{article.source_body}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="content-section">
|
|
||||||
<h2 class="section-title">Audio Transcript</h2>
|
|
||||||
<div class="pos-text">{JSON.stringify(article.target_body_transcript, null, 2)}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="content-section">
|
|
||||||
<h2 class="section-title">Generated Text with POS Tags</h2>
|
|
||||||
<div class="pos-text">{JSON.stringify(article.target_body_pos, null, 2)}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="content-section">
|
|
||||||
<h2 class="section-title">Translated Text with POS Tags</h2>
|
|
||||||
<div class="pos-text">{JSON.stringify(article.source_body_pos, null, 2)}</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* --- Content sections --- */
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding-top: var(--space-4);
|
|
||||||
border-top: 1px solid color-mix(in srgb, var(--color-outline-variant) 30%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-title-lg);
|
|
||||||
font-weight: var(--weight-semibold);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-lang {
|
|
||||||
font-family: var(--font-label);
|
|
||||||
font-size: var(--text-label-md);
|
|
||||||
font-weight: var(--weight-medium);
|
|
||||||
letter-spacing: var(--tracking-wide);
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Prose --- */
|
|
||||||
|
|
||||||
.prose {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: var(--text-body-lg);
|
|
||||||
line-height: var(--leading-relaxed);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose-target {
|
|
||||||
padding: var(--space-5) var(--space-6);
|
|
||||||
background-color: var(--color-surface-container-low);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
font-size: calc(var(--text-body-lg) * 1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Audio --- */
|
|
||||||
|
|
||||||
.audio-player {
|
|
||||||
width: 100%;
|
|
||||||
accent-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose-translated {
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
font-size: var(--text-body-md);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pos-text {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--text-body-sm);
|
|
||||||
line-height: var(--leading-relaxed);
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
background-color: var(--color-surface-container-low);
|
|
||||||
padding: var(--space-4);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 300px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -2,24 +2,16 @@ import adapter from '@sveltejs/adapter-node';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
experimental: {
|
alias: {
|
||||||
remoteFunctions: true
|
'@client': 'src/client/client.gen.ts'
|
||||||
},
|
}
|
||||||
alias: {
|
},
|
||||||
'@client': 'src/client/client.gen.ts'
|
vitePlugin: {
|
||||||
}
|
dynamicCompileOptions: ({ filename }) =>
|
||||||
},
|
filename.includes('node_modules') ? undefined : { runes: true }
|
||||||
compilerOptions: {
|
}
|
||||||
experimental: {
|
|
||||||
async: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vitePlugin: {
|
|
||||||
dynamicCompileOptions: ({ filename }) =>
|
|
||||||
filename.includes('node_modules') ? undefined : { runes: true }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue