Compare commits

..

1 Commits

Author SHA1 Message Date
minori a7d99f9f6e deps: update tailwindcss to 4.2.2
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m13s
CI / Lint & Check (pull_request) Failing after 6m33s
CI / Build Windows (pull_request) Has been skipped
2026-04-10 07:07:24 -07:00
15 changed files with 124 additions and 881 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "tatsumi", "name": "tatsumi",
"private": true, "private": true,
"version": "1.2.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -26,7 +26,7 @@
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"eslint": "9.25.1", "eslint": "9.25.1",
"postcss": "8.5.3", "postcss": "8.5.3",
"tailwindcss": "3.4.17", "tailwindcss": "4.2.2",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite": "6.3.2", "vite": "6.3.2",
"@vitejs/plugin-react": "4.4.1" "@vitejs/plugin-react": "4.4.1"
+9 -275
View File
@@ -52,8 +52,8 @@ importers:
specifier: 8.5.3 specifier: 8.5.3
version: 8.5.3 version: 8.5.3
tailwindcss: tailwindcss:
specifier: 3.4.17 specifier: 4.2.2
version: 3.4.17 version: 4.2.2
typescript: typescript:
specifier: 5.8.3 specifier: 5.8.3
version: 5.8.3 version: 5.8.3
@@ -63,10 +63,6 @@ importers:
packages: packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -874,20 +870,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'} engines: {node: '>=8'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
are-docs-informative@0.0.2: are-docs-informative@0.0.2:
resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
engines: {node: '>=14'} engines: {node: '>=14'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -958,10 +944,6 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@1.1.13: brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
@@ -1001,10 +983,6 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001787: caniuse-lite@1.0.30001787:
resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
@@ -1016,10 +994,6 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
ci-info@4.4.0: ci-info@4.4.0:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1035,10 +1009,6 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
comment-parser@1.4.1: comment-parser@1.4.1:
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -1056,11 +1026,6 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1104,16 +1069,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dir-glob@3.0.1: dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'} engines: {node: '>=8'}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
doctrine@2.1.0: doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1507,10 +1466,6 @@ packages:
resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-boolean-object@1.2.2: is-boolean-object@1.2.2:
resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1669,10 +1624,6 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -1730,9 +1681,6 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1751,10 +1699,6 @@ packages:
normalize-package-data@2.5.0: normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
normalize-range@0.1.2: normalize-range@0.1.2:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1763,10 +1707,6 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-inspect@1.13.4: object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1867,14 +1807,6 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
playwright-core@1.59.1: playwright-core@1.59.1:
resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1893,40 +1825,6 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.1.0:
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0: postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -1964,9 +1862,6 @@ packages:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
read-pkg-up@7.0.1: read-pkg-up@7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1975,10 +1870,6 @@ packages:
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
engines: {node: '>=8'} engines: {node: '>=8'}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2159,11 +2050,6 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-color@7.2.0: supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2176,17 +2062,8 @@ packages:
resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==} resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
tailwindcss@3.4.17: tailwindcss@4.2.2:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinybench@2.9.0: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -2219,9 +2096,6 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4' typescript: '>=4.8.4'
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tsconfig-paths@3.15.0: tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@@ -2278,9 +2152,6 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -2409,8 +2280,6 @@ packages:
snapshots: snapshots:
'@alloc/quick-lru@5.2.0': {}
'@babel/code-frame@7.29.0': '@babel/code-frame@7.29.0':
dependencies: dependencies:
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
@@ -3197,17 +3066,8 @@ snapshots:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.2
are-docs-informative@0.0.2: {} are-docs-informative@0.0.2: {}
arg@5.0.2: {}
argparse@2.0.1: {} argparse@2.0.1: {}
array-buffer-byte-length@1.0.2: array-buffer-byte-length@1.0.2:
@@ -3303,8 +3163,6 @@ snapshots:
baseline-browser-mapping@2.10.17: {} baseline-browser-mapping@2.10.17: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.13: brace-expansion@1.1.13:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -3351,8 +3209,6 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001787: {} caniuse-lite@1.0.30001787: {}
chai@6.2.2: {} chai@6.2.2: {}
@@ -3362,18 +3218,6 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
ci-info@4.4.0: {} ci-info@4.4.0: {}
clean-regexp@1.0.0: clean-regexp@1.0.0:
@@ -3386,8 +3230,6 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
commander@4.1.1: {}
comment-parser@1.4.1: {} comment-parser@1.4.1: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
@@ -3404,8 +3246,6 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
cssesc@3.0.0: {}
csstype@3.2.3: {} csstype@3.2.3: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@@ -3448,14 +3288,10 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
didyoumean@1.2.2: {}
dir-glob@3.0.1: dir-glob@3.0.1:
dependencies: dependencies:
path-type: 4.0.0 path-type: 4.0.0
dlv@1.1.3: {}
doctrine@2.1.0: doctrine@2.1.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@@ -4021,10 +3857,6 @@ snapshots:
dependencies: dependencies:
has-bigints: 1.1.0 has-bigints: 1.1.0
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-boolean-object@1.2.2: is-boolean-object@1.2.2:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -4132,7 +3964,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
set-function-name: 2.0.2 set-function-name: 2.0.2
jiti@1.21.7: {} jiti@1.21.7:
optional: true
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -4176,8 +4009,6 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
locate-path@5.0.0: locate-path@5.0.0:
@@ -4229,12 +4060,6 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.11: {} nanoid@3.3.11: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -4255,14 +4080,10 @@ snapshots:
semver: 5.7.2 semver: 5.7.2
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}
normalize-range@0.1.2: {} normalize-range@0.1.2: {}
object-assign@4.1.1: {} object-assign@4.1.1: {}
object-hash@3.0.0: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
object-keys@1.1.1: {} object-keys@1.1.1: {}
@@ -4370,10 +4191,6 @@ snapshots:
picomatch@4.0.4: {} picomatch@4.0.4: {}
pify@2.3.0: {}
pirates@4.0.7: {}
playwright-core@1.59.1: {} playwright-core@1.59.1: {}
playwright@1.59.1: playwright@1.59.1:
@@ -4386,35 +4203,6 @@ snapshots:
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
postcss-import@15.1.0(postcss@8.5.3):
dependencies:
postcss: 8.5.3
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.11
postcss-js@4.1.0(postcss@8.5.3):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.3
postcss-load-config@4.0.2(postcss@8.5.3):
dependencies:
lilconfig: 3.1.3
yaml: 2.8.3
optionalDependencies:
postcss: 8.5.3
postcss-nested@6.2.0(postcss@8.5.3):
dependencies:
postcss: 8.5.3
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {} postcss-value-parser@4.2.0: {}
postcss@8.5.3: postcss@8.5.3:
@@ -4446,10 +4234,6 @@ snapshots:
react@19.1.0: {} react@19.1.0: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
read-pkg-up@7.0.1: read-pkg-up@7.0.1:
dependencies: dependencies:
find-up: 4.1.0 find-up: 4.1.0
@@ -4463,10 +4247,6 @@ snapshots:
parse-json: 5.2.0 parse-json: 5.2.0
type-fest: 0.6.0 type-fest: 0.6.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.2
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.9 call-bind: 1.0.9
@@ -4720,16 +4500,6 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
tinyglobby: 0.2.16
ts-interface-checker: 0.1.13
supports-color@7.2.0: supports-color@7.2.0:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
@@ -4741,40 +4511,7 @@ snapshots:
'@pkgr/core': 0.1.2 '@pkgr/core': 0.1.2
tslib: 2.8.1 tslib: 2.8.1
tailwindcss@3.4.17: tailwindcss@4.2.2: {}
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.3
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.5.3
postcss-import: 15.1.0(postcss@8.5.3)
postcss-js: 4.1.0(postcss@8.5.3)
postcss-load-config: 4.0.2(postcss@8.5.3)
postcss-nested: 6.2.0(postcss@8.5.3)
postcss-selector-parser: 6.1.2
resolve: 1.22.11
sucrase: 3.35.1
transitivePeerDependencies:
- ts-node
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tinybench@2.9.0: {} tinybench@2.9.0: {}
@@ -4799,8 +4536,6 @@ snapshots:
dependencies: dependencies:
typescript: 5.8.3 typescript: 5.8.3
ts-interface-checker@0.1.13: {}
tsconfig-paths@3.15.0: tsconfig-paths@3.15.0:
dependencies: dependencies:
'@types/json5': 0.0.29 '@types/json5': 0.0.29
@@ -4872,8 +4607,6 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
util-deprecate@1.0.2: {}
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
dependencies: dependencies:
spdx-correct: 3.2.0 spdx-correct: 3.2.0
@@ -4971,6 +4704,7 @@ snapshots:
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@2.8.3: {} yaml@2.8.3:
optional: true
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
+1 -1
View File
@@ -3716,7 +3716,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tatsumi" name = "tatsumi"
version = "1.2.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"dirs 5.0.1", "dirs 5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tatsumi" name = "tatsumi"
version = "1.2.0" version = "1.0.0"
description = "Tatsumi - AI art generation using Google Gemini" description = "Tatsumi - AI art generation using Google Gemini"
authors = ["Naomi Carrigan"] authors = ["Naomi Carrigan"]
edition = "2021" edition = "2021"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

After

Width:  |  Height:  |  Size: 17 MiB

+23 -58
View File
@@ -11,11 +11,11 @@ Character design (always required):
- Wavy ashen brown hair (colour and texture fixed; hairstyle can vary) - Wavy ashen brown hair (colour and texture fixed; hairstyle can vary)
- Very pale skin tone - Very pale skin tone
- Vibrant sky-blue eyes — important, commonly missed - Vibrant sky-blue eyes — important, commonly missed
- Vampire fangs (only visible when mouth is open) - Vampire fangs
- Glasses (pink-framed preferred, other styles acceptable) - Glasses (pink-framed preferred, other styles acceptable)
- Painted fingernails and toenails (any colour, never unpolished) - Painted fingernails and toenails (any colour, never unpolished)
- Slender build - Slender build
- Full body visible in frame - Full body visible in frame; always barefoot, never wears socks
Composition (always required): Composition (always required):
- Single character only - Single character only
@@ -46,19 +46,12 @@ fn build_safety_settings() -> Value {
]) ])
} }
fn build_generation_config( fn build_generation_config(mode: &str) -> Value {
mode: &str,
aspect_ratio: Option<&str>,
image_size: &str,
) -> Value {
let image_config = match mode { let image_config = match mode {
"avatar" => json!({ "aspectRatio": "1:1", "imageSize": image_size }), "avatar" => json!({ "aspectRatio": "1:1", "imageSize": "4K" }),
"art" => { "art" => json!({ "aspectRatio": "16:9", "imageSize": "4K" }),
let ratio = aspect_ratio.unwrap_or("16:9");
json!({ "aspectRatio": ratio, "imageSize": image_size })
}
// replace mode: omit aspectRatio so the model infers it from the source image // replace mode: omit aspectRatio so the model infers it from the source image
_ => json!({ "imageSize": image_size }), _ => json!({ "imageSize": "4K" }),
}; };
json!({ json!({
"imageConfig": image_config, "imageConfig": image_config,
@@ -90,56 +83,37 @@ fn build_user_gemini_parts(
user_image_base64: &Option<String>, user_image_base64: &Option<String>,
user_image_mime: &Option<String>, user_image_mime: &Option<String>,
) -> Vec<Value> { ) -> Vec<Value> {
if let Some(image_data) = user_image_base64.as_deref() { if mode == "replace" && user_image_base64.is_some() {
let mime = user_image_mime.as_deref().unwrap_or("image/png"); let mime = user_image_mime.as_deref().unwrap_or("image/png");
let data = user_image_base64.as_deref().unwrap_or("");
let base_text = user_text.as_deref().unwrap_or(""); let base_text = user_text.as_deref().unwrap_or("");
let final_text = if mode == "replace" { let final_text = if base_text.is_empty() {
if base_text.is_empty() { REPLACE_MODE_APPEND.to_string()
REPLACE_MODE_APPEND.to_string()
} else {
format!("{}\n{}", base_text, REPLACE_MODE_APPEND)
}
} else { } else {
base_text.to_string() format!("{}\n{}", base_text, REPLACE_MODE_APPEND)
}; };
let mut parts = vec![json!({"inlineData": {"mimeType": mime, "data": image_data}})]; vec![
if !final_text.is_empty() { json!({"inlineData": {"mimeType": mime, "data": data}}),
parts.push(json!({"text": final_text})); json!({"text": final_text}),
} ]
parts
} else { } else {
// No image: text-only message // Art/avatar mode, or replace mode follow-up correction (text only)
let text = user_text.as_deref().unwrap_or(""); let text = user_text.as_deref().unwrap_or("");
vec![json!({"text": text})] vec![json!({"text": text})]
} }
} }
pub struct GeminiCallParams {
pub mode: String,
pub aspect_ratio: Option<String>,
pub image_size: String,
pub user_text: Option<String>,
pub user_image_base64: Option<String>,
pub user_image_mime: Option<String>,
}
pub async fn call_gemini( pub async fn call_gemini(
api_key: String, api_key: String,
mode: String,
history: Vec<ThreadMessage>, history: Vec<ThreadMessage>,
params: GeminiCallParams, user_text: Option<String>,
user_image_base64: Option<String>,
user_image_mime: Option<String>,
) -> Result<(Vec<MessagePart>, f64), String> { ) -> Result<(Vec<MessagePart>, f64), String> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let GeminiCallParams {
mode,
aspect_ratio,
image_size,
user_text,
user_image_base64,
user_image_mime,
} = params;
let is_first_message = history.is_empty(); let is_first_message = history.is_empty();
let mut contents: Vec<Value> = history let mut contents: Vec<Value> = history
@@ -182,11 +156,7 @@ pub async fn call_gemini(
contents.push(json!({"role": "user", "parts": user_parts})); contents.push(json!({"role": "user", "parts": user_parts}));
let generation_config = build_generation_config( let generation_config = build_generation_config(mode.as_str());
mode.as_str(),
aspect_ratio.as_deref(),
image_size.as_str(),
);
let safety_settings = build_safety_settings(); let safety_settings = build_safety_settings();
let request_body = json!({ let request_body = json!({
@@ -278,13 +248,8 @@ pub async fn call_gemini(
let candidates_tokens = usage["candidatesTokenCount"].as_u64().unwrap_or(0); let candidates_tokens = usage["candidatesTokenCount"].as_u64().unwrap_or(0);
let image_part_count = result_parts.iter().filter(|p| p.part_type == "image").count() as u64; let image_part_count = result_parts.iter().filter(|p| p.part_type == "image").count() as u64;
// Image output tokens per image vary by resolution, billed at $120/1M tokens // Image output tokens (4K = 2000 tokens each) billed at $120/1M tokens
let tokens_per_image: u64 = match image_size.as_str() { let image_output_tokens = image_part_count * 2_000_u64;
"1K" => 500,
"2K" => 1_000,
_ => 2_000, // 4K default
};
let image_output_tokens = image_part_count * tokens_per_image;
// Remaining candidates tokens are text/thinking, billed at $12/1M tokens // Remaining candidates tokens are text/thinking, billed at $12/1M tokens
let text_output_tokens = candidates_tokens.saturating_sub(image_output_tokens); let text_output_tokens = candidates_tokens.saturating_sub(image_output_tokens);
+3 -17
View File
@@ -1,7 +1,7 @@
mod gemini; mod gemini;
mod storage; mod storage;
use gemini::{call_gemini, read_reference_image_base64, GeminiCallParams}; use gemini::{call_gemini, read_reference_image_base64};
use serde::Serialize; use serde::Serialize;
use storage::{ use storage::{
delete_thread_from_disk, load_config_from_disk, load_threads_from_disk, save_config_to_disk, delete_thread_from_disk, load_config_from_disk, load_threads_from_disk, save_config_to_disk,
@@ -46,30 +46,16 @@ async fn save_config(config: Config) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[allow(clippy::too_many_arguments)]
async fn send_message( async fn send_message(
api_key: String, api_key: String,
mode: String, mode: String,
aspect_ratio: Option<String>,
image_size: String,
history: Vec<ThreadMessage>, history: Vec<ThreadMessage>,
user_text: Option<String>, user_text: Option<String>,
user_image_base64: Option<String>, user_image_base64: Option<String>,
user_image_mime: Option<String>, user_image_mime: Option<String>,
) -> Result<SendMessageResult, String> { ) -> Result<SendMessageResult, String> {
let (parts, cost_usd) = call_gemini( let (parts, cost_usd) =
api_key, call_gemini(api_key, mode, history, user_text, user_image_base64, user_image_mime).await?;
history,
GeminiCallParams {
mode,
aspect_ratio,
image_size,
user_text,
user_image_base64,
user_image_mime,
},
)
.await?;
Ok(SendMessageResult { parts, cost_usd }) Ok(SendMessageResult { parts, cost_usd })
} }
-8
View File
@@ -25,19 +25,11 @@ pub struct ThreadMessage {
pub cost_usd: Option<f64>, pub cost_usd: Option<f64>,
} }
fn default_image_size() -> String {
"4K".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thread { pub struct Thread {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub mode: String, pub mode: String,
#[serde(rename = "aspectRatio", skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
#[serde(rename = "imageSize", default = "default_image_size")]
pub image_size: String,
pub messages: Vec<ThreadMessage>, pub messages: Vec<ThreadMessage>,
#[serde(rename = "createdAt")] #[serde(rename = "createdAt")]
pub created_at: i64, pub created_at: i64,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "tatsumi", "productName": "tatsumi",
"version": "1.2.0", "version": "1.0.0",
"identifier": "com.naomi.tatsumi", "identifier": "com.naomi.tatsumi",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
+18 -129
View File
@@ -16,127 +16,17 @@ import { SettingsModal } from "./components/settingsModal.tsx";
import { Sidebar } from "./components/sidebar.tsx"; import { Sidebar } from "./components/sidebar.tsx";
import { ThreadView } from "./components/threadView.tsx"; import { ThreadView } from "./components/threadView.tsx";
import { WelcomeScreen } from "./components/welcomeScreen.tsx"; import { WelcomeScreen } from "./components/welcomeScreen.tsx";
import type { import type { Config, Mode, PendingInput, Thread } from "./types/index.ts";
AspectRatio,
Config,
ImageSize,
Mode,
PendingInput,
Thread,
} from "./types/index.ts";
const adjectives = [ const generateThreadName = (mode: Mode, text?: string): string => {
"Amber", if (
"Ancient", mode === "replace"
"Arctic", || text === undefined
"Azure", || text.trim().length === 0
"Blazing", ) {
"Celestial", return `Replace - ${new Date().toLocaleString()}`;
"Cobalt", }
"Crimson", return text.trim().slice(0, 40);
"Crystal",
"Dark",
"Distant",
"Ember",
"Emerald",
"Eternal",
"Ethereal",
"Fleeting",
"Frosted",
"Gilded",
"Golden",
"Hidden",
"Hollow",
"Indigo",
"Ivory",
"Jade",
"Lunar",
"Midnight",
"Mystic",
"Neon",
"Obsidian",
"Onyx",
"Opal",
"Pale",
"Pearl",
"Radiant",
"Rusted",
"Sapphire",
"Scarlet",
"Shattered",
"Silent",
"Silver",
"Spectral",
"Twilight",
"Velvet",
"Verdant",
"Veiled",
"Violet",
"Wandering",
"Wild",
"Woven",
"Arcane",
];
const nouns = [
"Abyss",
"Archive",
"Aria",
"Bloom",
"Cascade",
"Chronicle",
"Cipher",
"Compass",
"Crown",
"Dawn",
"Dream",
"Echo",
"Elegy",
"Ember",
"Equinox",
"Flame",
"Fragment",
"Garden",
"Horizon",
"Labyrinth",
"Lantern",
"Mirage",
"Mist",
"Murmur",
"Nexus",
"Nocturne",
"Oracle",
"Phantom",
"Prism",
"Requiem",
"Reverie",
"Ruin",
"Shadow",
"Shard",
"Silence",
"Solstice",
"Sonata",
"Specter",
"Storm",
"Tempest",
"Throne",
"Tide",
"Veil",
"Vortex",
"Whisper",
"Zenith",
"Sigil",
"Glyph",
"Dusk",
"Omen",
];
const generateThreadName = (): string => {
const adjectiveIndex = Math.floor(Math.random() * adjectives.length);
const nounIndex = Math.floor(Math.random() * nouns.length);
const adjective = adjectives[adjectiveIndex] ?? "Arcane";
const noun = nouns[nounIndex] ?? "Reverie";
return `${adjective} ${noun}`;
}; };
const generateId = (): string => { const generateId = (): string => {
@@ -177,7 +67,13 @@ const resolveUpdatedThread = (updatedThread: Thread): Thread => {
return updatedThread; return updatedThread;
} }
const name = generateThreadName(); const firstTextPart = firstMessage.parts.find((part) => {
return part.type === "text";
});
const name = generateThreadName(
updatedThread.mode,
firstTextPart?.text,
);
return { ...updatedThread, name }; return { ...updatedThread, name };
}; };
@@ -224,18 +120,11 @@ const app = (): JSX.Element => {
}); });
const handleNewThread = useCallback( const handleNewThread = useCallback(
( (mode: Mode): void => {
mode: Mode,
aspectRatio: AspectRatio | undefined,
imageSize: ImageSize,
): void => {
const now = Date.now(); const now = Date.now();
const createdThread: Thread = { const createdThread: Thread = {
/* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */
...(aspectRatio !== undefined && { aspectRatio }),
createdAt: now, createdAt: now,
id: generateId(), id: generateId(),
imageSize: imageSize,
messages: [], messages: [],
mode: mode, mode: mode,
name: `New ${mode} thread`, name: `New ${mode} thread`,
+40 -68
View File
@@ -19,12 +19,7 @@ import {
type JSX, type JSX,
type KeyboardEvent, type KeyboardEvent,
} from "react"; } from "react";
import type { import type { Mode, PendingInput } from "../types/index.ts";
AspectRatio,
ImageSize,
Mode,
PendingInput,
} from "../types/index.ts";
const dropZoneBaseClass = [ const dropZoneBaseClass = [
"border-2 border-dashed rounded-xl p-8", "border-2 border-dashed rounded-xl p-8",
@@ -118,19 +113,14 @@ const readClipboardImage = async(
onFileRead(new File([ blob ], "clipboard.png", { type: imageType })); onFileRead(new File([ blob ], "clipboard.png", { type: imageType }));
}; };
const modeLabelText = ( const modeLabelText = (mode: Mode): string => {
mode: Mode,
aspectRatio: AspectRatio | undefined,
imageSize: ImageSize,
): string => {
if (mode === "avatar") { if (mode === "avatar") {
return `🟣 Avatar Mode · ${imageSize}`; return "🟣 Avatar Mode (1:1)";
} }
if (mode === "art") { if (mode === "art") {
const ratio = aspectRatio ?? "16:9"; return "🩷 Art Mode (16:9)";
return `🩷 Art Mode (${ratio}) · ${imageSize}`;
} }
return `🔵 Replace Mode · ${imageSize}`; return "🔵 Replace Mode";
}; };
type OnSendCallback = ( type OnSendCallback = (
@@ -140,9 +130,7 @@ type OnSendCallback = (
)=> void; )=> void;
interface InputAreaProperties { interface InputAreaProperties {
readonly aspectRatio?: AspectRatio;
readonly hasMessages: boolean; readonly hasMessages: boolean;
readonly imageSize: ImageSize;
readonly initialImageBase64?: string; readonly initialImageBase64?: string;
readonly initialImageMime?: string; readonly initialImageMime?: string;
readonly initialImagePreview?: string; readonly initialImagePreview?: string;
@@ -156,9 +144,7 @@ interface InputAreaProperties {
/** /**
* Renders the input area for composing and sending messages. * Renders the input area for composing and sending messages.
* @param props - The component props. * @param props - The component props.
* @param props.aspectRatio - The thread's aspect ratio setting (Art mode only).
* @param props.hasMessages - Whether the thread already has messages (affects replace mode UI). * @param props.hasMessages - Whether the thread already has messages (affects replace mode UI).
* @param props.imageSize - The thread's resolution setting.
* @param props.initialImageBase64 - Initial base64 image data to pre-populate. * @param props.initialImageBase64 - Initial base64 image data to pre-populate.
* @param props.initialImageMime - Initial image MIME type to pre-populate. * @param props.initialImageMime - Initial image MIME type to pre-populate.
* @param props.initialImagePreview - Initial image preview URL to pre-populate. * @param props.initialImagePreview - Initial image preview URL to pre-populate.
@@ -170,9 +156,7 @@ interface InputAreaProperties {
* @returns The JSX element. * @returns The JSX element.
*/ */
const inputArea = ({ const inputArea = ({
aspectRatio,
hasMessages, hasMessages,
imageSize,
initialImageBase64, initialImageBase64,
initialImageMime, initialImageMime,
initialImagePreview, initialImagePreview,
@@ -272,8 +256,7 @@ const inputArea = ({
if (isInitialReplace && imageBase64 === undefined) { if (isInitialReplace && imageBase64 === undefined) {
return; return;
} }
const hasContent = text.trim().length > 0 || imageBase64 !== undefined; if (!isInitialReplace && text.trim().length === 0) {
if (!isInitialReplace && !hasContent) {
return; return;
} }
@@ -349,14 +332,11 @@ const inputArea = ({
: dropZoneInactiveClass, : dropZoneInactiveClass,
].join(" "); ].join(" ");
const hasNoContent = text.trim().length === 0 && imageBase64 === undefined;
const isSendDisabled = isLoading || hasNoContent;
return ( return (
<div className="border-t border-purple-900/30 p-4 bg-[#0f0a1a]"> <div className="border-t border-purple-900/30 p-4 bg-[#0f0a1a]">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-500 uppercase tracking-wider"> <span className="text-xs text-gray-500 uppercase tracking-wider">
{modeLabelText(mode, aspectRatio, imageSize)} {modeLabelText(mode)}
</span> </span>
</div> </div>
@@ -417,15 +397,6 @@ const inputArea = ({
type="file" type="file"
/> />
<textarea
className={textareaClass}
disabled={isLoading}
onChange={handleTextChange}
placeholder="Add notes to include with the image (optional)..."
rows={2}
value={text}
/>
<button <button
className={replaceButtonClass} className={replaceButtonClass}
disabled={isLoading || imageBase64 === undefined} disabled={isLoading || imageBase64 === undefined}
@@ -443,40 +414,41 @@ const inputArea = ({
</div> </div>
: <div className="flex flex-col gap-3"> : <div className="flex flex-col gap-3">
<div className="flex flex-col gap-2"> {mode === "replace"
{imagePreview === undefined ? <div className="flex flex-col gap-2">
? <button {imagePreview === undefined
className={pasteButtonClass} ? <button
onClick={handlePasteButtonClick} className={pasteButtonClass}
type="button" onClick={handlePasteButtonClick}
>
{mode === "replace"
? "📋 Paste replacement image (optional)"
: "📋 Paste image (optional)"}
</button>
: <div className="relative inline-block">
<img
alt="Upload preview"
className="max-h-32 rounded-lg border border-purple-700/40"
src={imagePreview}
/>
<button
className={clearButtonClass}
onClick={clearImage}
type="button" type="button"
> >
{"×"} {"📋 Paste replacement image (optional)"}
</button> </button>
</div>}
<input : <div className="relative inline-block">
accept="image/*" <img
className="hidden" alt="Upload preview"
onChange={handleFileChange} className="max-h-32 rounded-lg border border-purple-700/40"
ref={fileInputReference} src={imagePreview}
type="file" />
/> <button
</div> className={clearButtonClass}
onClick={clearImage}
type="button"
>
{"×"}
</button>
</div>}
<input
accept="image/*"
className="hidden"
onChange={handleFileChange}
ref={fileInputReference}
type="file"
/>
</div>
: null}
<div className="flex gap-3 items-end"> <div className="flex gap-3 items-end">
<textarea <textarea
@@ -492,7 +464,7 @@ const inputArea = ({
/> />
<button <button
className={sendButtonClass} className={sendButtonClass}
disabled={isSendDisabled} disabled={isLoading || text.trim().length === 0}
onClick={handleSend} onClick={handleSend}
type="button" type="button"
> >
+10 -267
View File
@@ -1,18 +1,12 @@
/** /**
* @file Modal for selecting a new thread generation mode and options. * @file Modal for selecting a new thread generation mode.
* @copyright Naomi Carrigan * @copyright Naomi Carrigan
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
/* eslint-disable max-lines-per-function -- Component JSX inherently requires many lines */ /* eslint-disable max-lines-per-function -- Component JSX inherently requires many lines */
/* eslint-disable max-lines -- Two-step modal requires many option definitions */ import { useCallback, type JSX, type MouseEvent } from "react";
import { import type { Mode } from "../types/index.ts";
useCallback,
useState,
type JSX,
type MouseEvent,
} from "react";
import type { AspectRatio, ImageSize, Mode } from "../types/index.ts";
interface ModeOption { interface ModeOption {
colour: string; colour: string;
@@ -22,25 +16,14 @@ interface ModeOption {
mode: Mode; mode: Mode;
} }
interface AspectRatioOption {
label: string;
value: AspectRatio;
}
interface ImageSizeOption {
description: string;
label: string;
value: ImageSize;
}
const avatarDescription = [ const avatarDescription = [
"Generate a square 1:1 portrait.", "Generate a square 1:1 portrait.",
"Perfect for profile pictures and avatars.", "Perfect for profile pictures and avatars.",
].join(" "); ].join(" ");
const artDescription = [ const artDescription = [
"Generate landscape or portrait artwork.", "Generate wide 16:9 landscape artwork.",
"Choose your aspect ratio and resolution.", "Great for wallpapers and banners.",
].join(" "); ].join(" ");
const replaceDescription = [ const replaceDescription = [
@@ -72,21 +55,6 @@ const modeOptionList: Array<ModeOption> = [
}, },
]; ];
const aspectRatioOptions: Array<AspectRatioOption> = [
{ label: "1:1 Square", value: "1:1" },
{ label: "4:3 Standard", value: "4:3" },
{ label: "3:4 Portrait", value: "3:4" },
{ label: "16:9 Widescreen", value: "16:9" },
{ label: "9:16 Wallpaper", value: "9:16" },
{ label: "21:9 Ultrawide", value: "21:9" },
];
const imageSizeOptions: Array<ImageSizeOption> = [
{ description: "Faster, cheaper", label: "1K", value: "1K" },
{ description: "Balanced", label: "2K", value: "2K" },
{ description: "Best quality", label: "4K", value: "4K" },
];
const colourMap: Record< const colourMap: Record<
string, string,
{ badge: string; button: string; hover: string } { badge: string; button: string; hover: string }
@@ -114,20 +82,6 @@ const isMode = (value: string): value is Mode => {
return validModesSet.has(value); return validModesSet.has(value);
}; };
const validAspectRatios = new Set<string>([
"1:1", "3:4", "4:3", "9:16", "16:9", "21:9",
]);
const isAspectRatio = (value: string): value is AspectRatio => {
return validAspectRatios.has(value);
};
const validImageSizes = new Set<string>([ "1K", "2K", "4K" ]);
const isImageSize = (value: string): value is ImageSize => {
return validImageSizes.has(value);
};
const overlayClass = [ const overlayClass = [
"fixed inset-0 bg-black/70 backdrop-blur-sm", "fixed inset-0 bg-black/70 backdrop-blur-sm",
"flex items-center justify-center z-50", "flex items-center justify-center z-50",
@@ -139,243 +93,32 @@ const panelClass = [
"shadow-2xl shadow-purple-900/30", "shadow-2xl shadow-purple-900/30",
].join(" "); ].join(" ");
const pillBaseClass = [
"px-4 py-2 rounded-xl border text-sm font-medium",
"transition-all duration-200 cursor-pointer",
].join(" ");
const pillActiveClass = "border-purple-500 bg-purple-600/30 text-white";
const pillInactiveClass = [
"border-purple-900/40 bg-transparent text-gray-400",
"hover:border-purple-500/60 hover:text-gray-200",
].join(" ");
const startButtonClass = [
"w-full bg-gradient-to-r from-purple-600 to-pink-600",
"hover:from-purple-500 hover:to-pink-500",
"text-white font-semibold py-3 rounded-xl",
"transition-all duration-200 mt-2",
].join(" ");
const sizeButtonActiveClass = "border-purple-500 bg-purple-600/30 text-white";
const sizeButtonInactiveClass = [
"border-purple-900/40 bg-transparent text-gray-400",
"hover:border-purple-500/60 hover:text-gray-200",
].join(" ");
const backButtonClass = [
"text-gray-400 hover:text-white",
"transition-colors text-xl",
].join(" ");
const sectionLabelClass = [
"text-sm text-gray-400",
"uppercase tracking-wider mb-3",
].join(" ");
interface NewThreadModalProperties { interface NewThreadModalProperties {
readonly onClose: ()=> void; readonly onClose: ()=> void;
readonly onSelect: ( readonly onSelect: (mode: Mode)=> void;
mode: Mode,
aspectRatio: AspectRatio | undefined,
imageSize: ImageSize,
)=> void;
} }
/** /**
* Renders the new thread modal for selecting a generation mode and options. * Renders the new thread modal for selecting a generation mode.
* @param props - The component props. * @param props - The component props.
* @param props.onClose - Callback to close the modal. * @param props.onClose - Callback to close the modal.
* @param props.onSelect - Callback when a mode and options are confirmed. * @param props.onSelect - Callback when a mode is selected.
* @returns The JSX element. * @returns The JSX element.
*/ */
const threadModal = ({ const threadModal = ({
onClose, onClose,
onSelect, onSelect,
}: NewThreadModalProperties): JSX.Element => { }: NewThreadModalProperties): JSX.Element => {
const [ step, setStep ] = useState<"mode" | "options">("mode");
const [ selectedMode, setSelectedMode ] = useState<Mode | undefined>(
undefined,
);
const [ aspectRatio, setAspectRatio ] = useState<AspectRatio>("16:9");
const [ imageSize, setImageSize ] = useState<ImageSize>("4K");
const handleModeSelect = useCallback( const handleModeSelect = useCallback(
(event: MouseEvent<HTMLButtonElement>): void => { (event: MouseEvent<HTMLButtonElement>): void => {
const rawMode = event.currentTarget.dataset.mode; const rawMode = event.currentTarget.dataset.mode;
if (rawMode !== undefined && isMode(rawMode)) { if (rawMode !== undefined && isMode(rawMode)) {
setSelectedMode(rawMode); onSelect(rawMode);
setStep("options");
} }
}, },
[], [ onSelect ],
); );
const handleBack = useCallback((): void => {
setStep("mode");
}, []);
const handleStart = useCallback((): void => {
if (selectedMode === undefined) {
return;
}
const ratio = selectedMode === "art"
? aspectRatio
: undefined;
onSelect(selectedMode, ratio, imageSize);
}, [ selectedMode, aspectRatio, imageSize, onSelect ]);
const handleAspectRatioSelect = useCallback(
(event: MouseEvent<HTMLButtonElement>): void => {
const rawRatio = event.currentTarget.dataset.ratio;
if (rawRatio !== undefined && isAspectRatio(rawRatio)) {
setAspectRatio(rawRatio);
}
},
[],
);
const handleImageSizeSelect = useCallback(
(event: MouseEvent<HTMLButtonElement>): void => {
const rawSize = event.currentTarget.dataset.size;
if (rawSize !== undefined && isImageSize(rawSize)) {
setImageSize(rawSize);
}
},
[],
);
const selectedModeOption = modeOptionList.find((o) => {
return o.mode === selectedMode;
});
const selectedColours = selectedModeOption === undefined
? colourMap.purple
: colourMap[selectedModeOption.colour];
if (step === "options" && selectedModeOption !== undefined) {
return (
<div className={overlayClass}>
<div className={panelClass}>
<div className="flex items-center gap-3 mb-6">
<button
className={backButtonClass}
onClick={handleBack}
type="button"
>
{"←"}
</button>
<h2 className="text-2xl font-bold text-white flex-1">
{"Configure Thread"}
</h2>
<button
className="text-gray-400 hover:text-white transition-colors"
onClick={onClose}
type="button"
>
{"×"}
</button>
</div>
<div
className={[
"flex items-center gap-3 mb-6 p-4",
"rounded-xl bg-[#241836] border border-purple-900/30",
].join(" ")}
>
<span className="text-3xl">{selectedModeOption.icon}</span>
<div>
<div className="flex items-center gap-2">
<span className="text-white font-semibold">
{selectedModeOption.label}
</span>
<span
className={`text-xs px-2 py-0.5 rounded-full ${selectedColours?.badge ?? ""}`}
>
{selectedModeOption.mode}
</span>
</div>
<p className="text-gray-400 text-xs mt-0.5">
{selectedModeOption.description}
</p>
</div>
</div>
{selectedMode === "art"
? <div className="mb-6">
<p className={sectionLabelClass}>
{"Aspect Ratio"}
</p>
<div className="flex flex-wrap gap-2">
{aspectRatioOptions.map((option) => {
const pillClass = [
pillBaseClass,
aspectRatio === option.value
? pillActiveClass
: pillInactiveClass,
].join(" ");
return (
<button
className={pillClass}
data-ratio={option.value}
key={option.value}
onClick={handleAspectRatioSelect}
type="button"
>
{option.label}
</button>
);
})}
</div>
</div>
: null}
<div className="mb-6">
<p className={sectionLabelClass}>
{"Resolution"}
</p>
<div className="flex gap-3">
{imageSizeOptions.map((option) => {
const sizeClass = [
"flex-1 flex flex-col items-center py-3 px-2",
"rounded-xl border text-sm font-medium",
"transition-all duration-200 cursor-pointer",
imageSize === option.value
? sizeButtonActiveClass
: sizeButtonInactiveClass,
].join(" ");
return (
<button
className={sizeClass}
data-size={option.value}
key={option.value}
onClick={handleImageSizeSelect}
type="button"
>
<span className="text-base font-bold">{option.label}</span>
<span className="text-xs mt-0.5 opacity-70">
{option.description}
</span>
</button>
);
})}
</div>
</div>
<button
className={startButtonClass}
onClick={handleStart}
type="button"
>
{"Start Thread →"}
</button>
</div>
</div>
);
}
return ( return (
<div className={overlayClass}> <div className={overlayClass}>
<div className={panelClass}> <div className={panelClass}>
+9 -34
View File
@@ -68,19 +68,19 @@ const modeLabel = (mode: Mode): string => {
interface BuildUserPartsOptions { interface BuildUserPartsOptions {
readonly imageBase64?: string; readonly imageBase64?: string;
readonly imageMime?: string; readonly imageMime?: string;
readonly mode: Mode;
readonly text: string; readonly text: string;
} }
const buildUserParts = ({ const buildUserParts = ({
imageBase64, imageBase64,
imageMime, imageMime,
mode,
text, text,
}: BuildUserPartsOptions): Array<MessagePart> => { }: BuildUserPartsOptions): Array<MessagePart> => {
const userParts: Array<MessagePart> = []; const userParts: Array<MessagePart> = [];
if (imageBase64 === undefined) { if (mode === "replace" && imageBase64 !== undefined) {
userParts.push({ text: text, type: "text" });
} else {
userParts.push({ userParts.push({
imageData: imageBase64, imageData: imageBase64,
mimeType: imageMime ?? "image/png", mimeType: imageMime ?? "image/png",
@@ -89,6 +89,8 @@ const buildUserParts = ({
if (text.length > 0) { if (text.length > 0) {
userParts.push({ text: text, type: "text" }); userParts.push({ text: text, type: "text" });
} }
} else {
userParts.push({ text: text, type: "text" });
} }
return userParts; return userParts;
@@ -172,18 +174,13 @@ const threadView = ({
onLoadingChange(true); onLoadingChange(true);
onErrorChange(undefined); onErrorChange(undefined);
const { const { messages, mode } = thread;
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const userParts = buildUserParts({ const userParts = buildUserParts({
/* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */ /* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */
...(imageBase64 !== undefined && { imageBase64 }), ...(imageBase64 !== undefined && { imageBase64 }),
/* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */ /* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */
...(imageMime !== undefined && { imageMime }), ...(imageMime !== undefined && { imageMime }),
mode,
text, text,
}); });
@@ -220,9 +217,7 @@ const threadView = ({
"send_message", "send_message",
{ {
apiKey, apiKey,
aspectRatio,
history, history,
imageSize,
mode, mode,
userImageBase64, userImageBase64,
userImageMime, userImageMime,
@@ -258,13 +253,7 @@ const threadView = ({
const handleRetry = useCallback( const handleRetry = useCallback(
(modelMessageIndex: number): void => { (modelMessageIndex: number): void => {
const { const { messages, mode } = thread;
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const userMessageIndex = modelMessageIndex - 1; const userMessageIndex = modelMessageIndex - 1;
const userMessage = messages[userMessageIndex]; const userMessage = messages[userMessageIndex];
@@ -311,9 +300,7 @@ const threadView = ({
} }
const response = await invoke<SendResult>("send_message", { const response = await invoke<SendResult>("send_message", {
apiKey, apiKey,
aspectRatio,
history, history,
imageSize,
mode, mode,
userImageBase64, userImageBase64,
userImageMime, userImageMime,
@@ -366,13 +353,7 @@ const threadView = ({
const handleEditCommit = useCallback( const handleEditCommit = useCallback(
(messageIndex: number, editedText: string): void => { (messageIndex: number, editedText: string): void => {
const { const { messages, mode } = thread;
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const originalMessage = messages[messageIndex]; const originalMessage = messages[messageIndex];
if (originalMessage === undefined) { if (originalMessage === undefined) {
return; return;
@@ -423,9 +404,7 @@ const threadView = ({
} }
const response = await invoke<SendResult>("send_message", { const response = await invoke<SendResult>("send_message", {
apiKey, apiKey,
aspectRatio,
history, history,
imageSize,
mode, mode,
userImageBase64, userImageBase64,
userImageMime, userImageMime,
@@ -557,11 +536,7 @@ const threadView = ({
{...(pendingInput?.text !== undefined && { {...(pendingInput?.text !== undefined && {
initialText: pendingInput.text, initialText: pendingInput.text,
})} })}
{...(thread.aspectRatio !== undefined && {
aspectRatio: thread.aspectRatio,
})}
hasMessages={thread.messages.length > 0} hasMessages={thread.messages.length > 0}
imageSize={thread.imageSize ?? "4K"}
isLoading={isLoading} isLoading={isLoading}
mode={thread.mode} mode={thread.mode}
onInputChange={onPendingInputChange} onInputChange={onPendingInputChange}
+7 -20
View File
@@ -5,8 +5,6 @@
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
type AspectRatio = "1:1" | "3:4" | "4:3" | "9:16" | "16:9" | "21:9";
type ImageSize = "1K" | "2K" | "4K";
type Mode = "avatar" | "art" | "replace"; type Mode = "avatar" | "art" | "replace";
interface Config { interface Config {
@@ -35,23 +33,12 @@ interface ThreadMessage {
} }
interface Thread { interface Thread {
aspectRatio?: AspectRatio; createdAt: number;
createdAt: number; id: string;
id: string; messages: Array<ThreadMessage>;
imageSize?: ImageSize; mode: Mode;
messages: Array<ThreadMessage>; name: string;
mode: Mode; updatedAt: number;
name: string;
updatedAt: number;
} }
export type { export type { Config, MessagePart, Mode, PendingInput, Thread, ThreadMessage };
AspectRatio,
Config,
ImageSize,
MessagePart,
Mode,
PendingInput,
Thread,
ThreadMessage,
};