generated from nhcarrigan/template
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34fa1e5fe8 |
+1
-1
@@ -29,6 +29,6 @@
|
|||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"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": "6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+20
-265
@@ -40,8 +40,8 @@ importers:
|
|||||||
specifier: 19.1.2
|
specifier: 19.1.2
|
||||||
version: 19.1.2(@types/react@19.1.2)
|
version: 19.1.2(@types/react@19.1.2)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: 4.4.1
|
specifier: 6.0.1
|
||||||
version: 4.4.1(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3))
|
version: 6.0.1(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3))
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: 10.4.21
|
specifier: 10.4.21
|
||||||
version: 10.4.21(postcss@8.5.3)
|
version: 10.4.21(postcss@8.5.3)
|
||||||
@@ -71,85 +71,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/compat-data@7.29.0':
|
|
||||||
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/core@7.29.0':
|
|
||||||
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/generator@7.29.1':
|
|
||||||
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-compilation-targets@7.28.6':
|
|
||||||
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0':
|
|
||||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.28.6':
|
|
||||||
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-module-transforms@7.28.6':
|
|
||||||
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@babel/core': ^7.0.0
|
|
||||||
|
|
||||||
'@babel/helper-plugin-utils@7.28.6':
|
|
||||||
resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1':
|
|
||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
'@babel/helper-validator-identifier@7.28.5':
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1':
|
|
||||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helpers@7.29.2':
|
|
||||||
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/parser@7.29.2':
|
|
||||||
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-self@7.27.1':
|
|
||||||
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@babel/core': ^7.0.0-0
|
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-source@7.27.1':
|
|
||||||
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@babel/core': ^7.0.0-0
|
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
|
||||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/traverse@7.29.0':
|
|
||||||
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/types@7.29.0':
|
|
||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@es-joy/jsdoccomment@0.49.0':
|
'@es-joy/jsdoccomment@0.49.0':
|
||||||
resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==}
|
resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -390,9 +315,6 @@ packages:
|
|||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
'@jridgewell/remapping@2.3.5':
|
|
||||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -435,6 +357,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.7':
|
||||||
|
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||||
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@@ -655,18 +580,6 @@ packages:
|
|||||||
'@tauri-apps/plugin-fs@2.4.0':
|
'@tauri-apps/plugin-fs@2.4.0':
|
||||||
resolution: {integrity: sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g==}
|
resolution: {integrity: sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
|
||||||
|
|
||||||
'@types/babel__generator@7.27.0':
|
|
||||||
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
|
|
||||||
|
|
||||||
'@types/babel__template@7.4.4':
|
|
||||||
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
|
|
||||||
|
|
||||||
'@types/babel__traverse@7.28.0':
|
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
@@ -804,11 +717,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==}
|
resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.4.1':
|
'@vitejs/plugin-react@6.0.1':
|
||||||
resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==}
|
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
|
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
|
||||||
|
babel-plugin-react-compiler: ^1.0.0
|
||||||
|
vite: ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@rolldown/plugin-babel':
|
||||||
|
optional: true
|
||||||
|
babel-plugin-react-compiler:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/eslint-plugin@1.1.24':
|
'@vitest/eslint-plugin@1.1.24':
|
||||||
resolution: {integrity: sha512-7IaENe4NNy33g0iuuy5bHY69JYYRjpv4lMx6H5Wp30W7ez2baLHwxsXF5TM4wa8JDYZt8ut99Ytoj7GiDO01hw==}
|
resolution: {integrity: sha512-7IaENe4NNy33g0iuuy5bHY69JYYRjpv4lMx6H5Wp30W7ez2baLHwxsXF5TM4wa8JDYZt8ut99Ytoj7GiDO01hw==}
|
||||||
@@ -1391,10 +1311,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2:
|
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1653,11 +1569,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
json5@2.2.3:
|
|
||||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -1691,9 +1602,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
|
||||||
|
|
||||||
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==}
|
||||||
|
|
||||||
@@ -1956,10 +1864,6 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
react-refresh@0.17.0:
|
|
||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
react@19.1.0:
|
react@19.1.0:
|
||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2395,9 +2299,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
yallist@3.1.1:
|
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
|
||||||
|
|
||||||
yaml@2.8.3:
|
yaml@2.8.3:
|
||||||
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
|
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
|
||||||
engines: {node: '>= 14.6'}
|
engines: {node: '>= 14.6'}
|
||||||
@@ -2417,112 +2318,8 @@ snapshots:
|
|||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
'@babel/compat-data@7.29.0': {}
|
|
||||||
|
|
||||||
'@babel/core@7.29.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.29.0
|
|
||||||
'@babel/generator': 7.29.1
|
|
||||||
'@babel/helper-compilation-targets': 7.28.6
|
|
||||||
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
|
|
||||||
'@babel/helpers': 7.29.2
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/template': 7.28.6
|
|
||||||
'@babel/traverse': 7.29.0
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
'@jridgewell/remapping': 2.3.5
|
|
||||||
convert-source-map: 2.0.0
|
|
||||||
debug: 4.4.3
|
|
||||||
gensync: 1.0.0-beta.2
|
|
||||||
json5: 2.2.3
|
|
||||||
semver: 6.3.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/generator@7.29.1':
|
|
||||||
dependencies:
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
jsesc: 3.1.0
|
|
||||||
|
|
||||||
'@babel/helper-compilation-targets@7.28.6':
|
|
||||||
dependencies:
|
|
||||||
'@babel/compat-data': 7.29.0
|
|
||||||
'@babel/helper-validator-option': 7.27.1
|
|
||||||
browserslist: 4.28.2
|
|
||||||
lru-cache: 5.1.1
|
|
||||||
semver: 6.3.1
|
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0': {}
|
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.28.6':
|
|
||||||
dependencies:
|
|
||||||
'@babel/traverse': 7.29.0
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.29.0
|
|
||||||
'@babel/helper-module-imports': 7.28.6
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
'@babel/traverse': 7.29.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/helper-plugin-utils@7.28.6': {}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1': {}
|
|
||||||
|
|
||||||
'@babel/helpers@7.29.2':
|
|
||||||
dependencies:
|
|
||||||
'@babel/template': 7.28.6
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@babel/parser@7.29.2':
|
|
||||||
dependencies:
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.29.0
|
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.29.0
|
|
||||||
'@babel/helper-plugin-utils': 7.28.6
|
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.29.0
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@babel/traverse@7.29.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.29.0
|
|
||||||
'@babel/generator': 7.29.1
|
|
||||||
'@babel/helper-globals': 7.28.0
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/template': 7.28.6
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
debug: 4.4.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/types@7.29.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-string-parser': 7.27.1
|
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
|
||||||
|
|
||||||
'@es-joy/jsdoccomment@0.49.0':
|
'@es-joy/jsdoccomment@0.49.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
comment-parser: 1.4.1
|
comment-parser: 1.4.1
|
||||||
@@ -2693,11 +2490,6 @@ snapshots:
|
|||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
'@jridgewell/remapping@2.3.5':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
@@ -2754,6 +2546,8 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.1.2': {}
|
'@pkgr/core@0.1.2': {}
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2904,27 +2698,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
|
||||||
dependencies:
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
'@types/babel__generator': 7.27.0
|
|
||||||
'@types/babel__template': 7.4.4
|
|
||||||
'@types/babel__traverse': 7.28.0
|
|
||||||
|
|
||||||
'@types/babel__generator@7.27.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@types/babel__template@7.4.4':
|
|
||||||
dependencies:
|
|
||||||
'@babel/parser': 7.29.2
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@types/babel__traverse@7.28.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/types': 7.29.0
|
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/deep-eql': 4.0.2
|
'@types/deep-eql': 4.0.2
|
||||||
@@ -3114,16 +2887,10 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.58.1
|
'@typescript-eslint/types': 8.58.1
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.4.1(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3))':
|
'@vitejs/plugin-react@6.0.1(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
|
||||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
|
|
||||||
'@types/babel__core': 7.20.5
|
|
||||||
react-refresh: 0.17.0
|
|
||||||
vite: 6.3.2(jiti@1.21.7)(yaml@2.8.3)
|
vite: 6.3.2(jiti@1.21.7)(yaml@2.8.3)
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.58.1(eslint@9.25.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.25.1(jiti@1.21.7))(typescript@5.8.3)(vitest@4.1.4(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3)))':
|
'@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.58.1(eslint@9.25.1(jiti@1.21.7))(typescript@5.8.3))(eslint@9.25.1(jiti@1.21.7))(typescript@5.8.3)(vitest@4.1.4(vite@6.3.2(jiti@1.21.7)(yaml@2.8.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3900,8 +3667,6 @@ snapshots:
|
|||||||
|
|
||||||
generator-function@2.0.1: {}
|
generator-function@2.0.1: {}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -4158,8 +3923,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
json5@2.2.3: {}
|
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -4194,10 +3957,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
|
||||||
dependencies:
|
|
||||||
yallist: 3.1.1
|
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -4442,8 +4201,6 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-refresh@0.17.0: {}
|
|
||||||
|
|
||||||
react@19.1.0: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
read-cache@1.0.0:
|
read-cache@1.0.0:
|
||||||
@@ -4969,8 +4726,6 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
|
||||||
|
|
||||||
yaml@2.8.3: {}
|
yaml@2.8.3: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 155 KiB |
+21
-56
@@ -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
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+18
-129
@@ -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`,
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
@@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user