Compare commits
1321 Commits
v0.23.0
...
388b0f323b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
388b0f323b | ||
| 6c89c3c512 | |||
|
|
7da3ce57ce | ||
| bb9eb0e108 | |||
|
|
b6aa3a32be | ||
| 6420c39b50 | |||
|
|
46289a16f4 | ||
| bc503dae6c | |||
|
|
cbf646420c | ||
| 41febf031b | |||
| ae7118f739 | |||
| c2eb814f63 | |||
|
|
7c23c7860f | ||
| bd5efa1fff | |||
|
|
c60d643b97 | ||
| 5d219dc450 | |||
| 5e9eff53ad | |||
|
|
9d3a97a61c | ||
| 5888589c32 | |||
| 7a612925dc | |||
| eb34a4d379 | |||
| a7dee2fae4 | |||
| 6731b15265 | |||
| d7f00fc529 | |||
| 2f6bad6710 | |||
| ee92afc00b | |||
| 134fc61c6a | |||
| e9f9b3fa5b | |||
|
|
633ad0ce2d | ||
| 2e72ad57f5 | |||
|
|
9f86343f59 | ||
| fb44dcce25 | |||
|
|
419128e7ce | ||
| d41e5964cc | |||
|
|
26a1a76875 | ||
| b5e4023b0f | |||
|
|
8a82fe302a | ||
| ffa4e24548 | |||
|
|
c6f7352a4a | ||
| 310f58e28b | |||
|
|
bf7c816e1a | ||
| 9a9c3de778 | |||
|
|
bda049af08 | ||
| 106f372cde | |||
|
|
94efb3d823 | ||
| a4420237ae | |||
|
|
f8a928682e | ||
| 92dd301cf0 | |||
| b6646cdb17 | |||
|
|
0934cfdd67 | ||
| aaa804ec7a | |||
|
|
1c603b79dc | ||
| dd414d3dec | |||
| e54d221526 | |||
|
|
ed2c9555b9 | ||
| 59ede04467 | |||
|
|
2c84265462 | ||
| 5fb87dee6d | |||
| 813e7e32de | |||
|
|
3211c6e710 | ||
| 39b84acca2 | |||
| 2aa094a9a1 | |||
| fb575801c5 | |||
| 4d04d1baf2 | |||
|
|
de13a2ba60 | ||
| 7da6f94abf | |||
| cf81d5e106 | |||
|
|
7e321bda2e | ||
| a7e5b597df | |||
| eff4a38521 | |||
| 220abf2805 | |||
|
|
9c0c91f37c | ||
| a882f48a42 | |||
| 75b6feee82 | |||
| cd2613429d | |||
|
|
aa78f7929c | ||
| 762a4a66c3 | |||
| c44a092186 | |||
| 463fd4035d | |||
|
|
48d587af79 | ||
| a91cd0b9a1 | |||
| 3fbbf8481b | |||
|
|
f87e8ce1d7 | ||
| 64151baeb0 | |||
| 2f2bcd3d99 | |||
|
|
aa717e6ac2 | ||
| b0ae63fb6a | |||
| 485f5cc44b | |||
| d55b5d2d1f | |||
| a2f203ee33 | |||
| a5de05356f | |||
| 4da4123b3b | |||
| a3950d64b3 | |||
| 25994bbdcd | |||
| bcfd3a83b8 | |||
|
|
b71ec94afd | ||
| 6a3f0d5b63 | |||
| 5ac489e8e2 | |||
|
|
08b63271f5 | ||
| 287f8317fa | |||
|
|
59bcaa2189 | ||
| fc625236df | |||
|
|
c46f864470 | ||
| 01656fcb0e | |||
|
|
eaf8a8b299 | ||
| f19d996ff5 | |||
|
|
cd421bd15d | ||
| 7820ec2a12 | |||
|
|
f56f32c990 | ||
| 713f776f76 | |||
| 6eef6cc370 | |||
| 275dc4ccf4 | |||
|
|
8dcaa2d6e8 | ||
| 6296d841c1 | |||
|
|
0063af9c0a | ||
| b68afae5fa | |||
|
|
69db94e4ed | ||
| 0d1d7638b5 | |||
| ef484af66c | |||
| 56a57b3bd0 | |||
|
|
ae84b617d0 | ||
| b1eda6fa34 | |||
| 5b55edb038 | |||
| 36b4b92fb5 | |||
|
|
3162eab737 | ||
| 7cd211b157 | |||
| 075e0e97b9 | |||
| 16e270794d | |||
| 399d00b0b1 | |||
| 14e88fca85 | |||
| 4f0b399b9c | |||
| 21d71ce62e | |||
| b3a64e6ceb | |||
| 2c55173982 | |||
| 2988992298 | |||
|
|
fdb49a8ef3 | ||
| ffa7e80d0c | |||
| 24feccda70 | |||
| ae624bf297 | |||
|
|
a9fd98bfc1 | ||
| 2a95eea1ec | |||
| f7c4dc57ac | |||
| d7e0ad9da5 | |||
|
|
53d17bc072 | ||
| 415f6ec661 | |||
|
|
1d78251598 | ||
| ba3ccc329c | |||
|
|
411545f63e | ||
| 568a9a9342 | |||
|
|
721f51f7a3 | ||
| 7bcaf0928f | |||
| d0b4a66d6e | |||
| 97e1e0bd30 | |||
|
|
a4fd27390b | ||
| aff7f25910 | |||
| dc0d0e2d7c | |||
|
|
0e3ce556d8 | ||
| cb05bcd5b8 | |||
|
|
14883755fd | ||
| b4bedec1c5 | |||
| 9f1fa5ff2d | |||
|
|
8183d02e1c | ||
| 8a1d7e5dc5 | |||
| 4e7ff5a291 | |||
|
|
a4ece6bfe1 | ||
| 415e99f13f | |||
| e1cbfac55e | |||
|
|
867364205d | ||
| db40f00841 | |||
| dcb842178d | |||
|
|
c37a88a367 | ||
| f29897adb0 | |||
| 2c89795146 | |||
|
|
2ccef627b5 | ||
| 5e465dc8c7 | |||
| bc7ec7969c | |||
|
|
5b76e9f310 | ||
| edf3b25990 | |||
|
|
797bea6398 | ||
| 8aeda2a06c | |||
| 4e26a573f5 | |||
|
|
1ab8f40b0d | ||
| 29117fb4d5 | |||
|
|
a8cfb037a9 | ||
| 6e1405edba | |||
|
|
9c4b33b969 | ||
| bc492fb93e | |||
|
|
b47bcf0fe3 | ||
| eee0987f31 | |||
| 7a19e335b8 | |||
|
|
73977e6bfe | ||
| 895fa30540 | |||
|
|
22ec4888fc | ||
| 528b9d9f56 | |||
|
|
10d7eadc64 | ||
| c1b808217c | |||
|
|
7d731a4c23 | ||
| df259f4210 | |||
|
|
244a54463b | ||
| f3e2266a4a | |||
|
|
fd646b3f37 | ||
| 07256e45bb | |||
|
|
711ed3c885 | ||
| 7692b5334d | |||
| 1f2b83c898 | |||
|
|
7e9edc8991 | ||
| 9509085783 | |||
|
|
dfb75c94fe | ||
| fe25d2ec50 | |||
|
|
260a5b8b7c | ||
| b8e4d00f0e | |||
| 8cc357072c | |||
| 4c7bf4b08a | |||
|
|
0f7db1a7df | ||
| 3b887ed1e9 | |||
|
|
e468e29169 | ||
| 7447ef40e2 | |||
| 5b158bb193 | |||
|
|
46b32341ac | ||
| 06157d1aa2 | |||
|
|
abd1e4c439 | ||
| 4336504ffa | |||
|
|
1be1a56cdd | ||
| cba801a155 | |||
| 2d3358cc51 | |||
|
|
c46f12f885 | ||
| 7bb0fc20d2 | |||
| 17c03a5971 | |||
| 77cf06e90f | |||
|
|
4c1f07bfe2 | ||
| 9b5a4d18ca | |||
|
|
32f8bc1648 | ||
| f6a6780470 | |||
| bae8f0ff15 | |||
| c3a3baee3f | |||
|
|
6471d78d70 | ||
| b7db5bab87 | |||
|
|
fbc814b2bf | ||
| afdcd8ed17 | |||
|
|
04bb8cfb81 | ||
| e21ae1dc34 | |||
|
|
fcbf0922f4 | ||
| 8ded450e4f | |||
|
|
9df5dd283e | ||
| c93730a084 | |||
|
|
44b07db7bb | ||
| 4e4cc901be | |||
|
|
29c504a25a | ||
| bed8810689 | |||
|
|
656aca7b35 | ||
| 99068642bf | |||
| 249f67a0f9 | |||
|
|
8b3c5116ab | ||
| bbec9d251f | |||
|
|
40961c6d42 | ||
| 006c63ad47 | |||
| 703aac1dd5 | |||
|
|
95170689f0 | ||
| daf912eb89 | |||
|
|
fd1512c2c0 | ||
| b00a826df7 | |||
|
|
e78f1c9c89 | ||
| 7b5584ae2c | |||
|
|
6f2e91dcae | ||
| ade8f5862f | |||
|
|
3c629fa876 | ||
| d064eb64d5 | |||
| a3a62c75b5 | |||
| 9d43182532 | |||
|
|
0d34b4b5ab | ||
| ca9c53feec | |||
| dd7a3e0152 | |||
| e872280fea | |||
| ec6a2ea3c2 | |||
|
|
31c24ba50d | ||
| 9fc6c5632c | |||
| 8cc6558231 | |||
| ea5076afad | |||
| af5f0e52b9 | |||
| 7be974a915 | |||
| 8c788c5163 | |||
|
|
22d8c22292 | ||
| 11482dc6c5 | |||
|
|
8c83277b8e | ||
| e1e188a5ec | |||
|
|
c3d4a8f943 | ||
| 18f90983b5 | |||
|
|
ca1fc442c4 | ||
| 1a49ab8658 | |||
|
|
723c162cb4 | ||
| f58de45bd1 | |||
|
|
fe75fb358e | ||
| 8da52c7b6b | |||
| dc7ec7ecd7 | |||
| 276964bfe7 | |||
|
|
978fd3e16c | ||
| 9db195e775 | |||
| 2fb2cdaab0 | |||
| 2264fe3c33 | |||
| 6d8a1a0d72 | |||
| a733c476e0 | |||
| 37b9aa6d3b | |||
| b10fd18b3a | |||
| fc7c6b63a0 | |||
| 8d05208e88 | |||
| 8b6c0e9058 | |||
| 73d209bacc | |||
| 31b98171df | |||
| 3bec3a4509 | |||
| ad4b781e4f | |||
| 2636a5e32c | |||
| 5f46e326e3 | |||
| d36a7259c0 | |||
| acb6c2b722 | |||
| 2a16bc965c | |||
|
|
566f50638d | ||
| 619054205c | |||
| ef5d8ab51d | |||
| e10552d076 | |||
| 2e9cf65ccc | |||
| 2d36d8739b | |||
| fb4fa1d820 | |||
| 210a7bdc6d | |||
| b100371627 | |||
| 424a8954b3 | |||
|
|
ef8936061e | ||
| 4674e58015 | |||
| 50c4093eec | |||
| 8da3f879b3 | |||
| 6ca9ad3a54 | |||
| f0affed9d2 | |||
| 9cb7376baa | |||
| 8b4eb2c19b | |||
| 993870e74e | |||
| fa0331fa1e | |||
| bf9adb78f8 | |||
| d07801d0e1 | |||
| 1ef940ade2 | |||
| 34b2998b48 | |||
| 48b0d87f4c | |||
| b8fe9981dc | |||
| 6b47b7ac36 | |||
| 664f25a3b4 | |||
| 0a142d17a1 | |||
| 48b67af661 | |||
| c28a680adf | |||
| beb694bfe3 | |||
| 8ae99b8480 | |||
| 29b87e3f60 | |||
| 4db3ff0fd9 | |||
| 8522a2b726 | |||
| 539adea6bb | |||
| 9e90b1b297 | |||
|
|
1e22e326b4 | ||
| 9e048b0b66 | |||
|
|
76b5f102f5 | ||
| 479f99d534 | |||
| 5091b1477a | |||
| 7627d9c532 | |||
| 3cb44315ad | |||
| c1fd7deabb | |||
|
|
f655c1ee84 | ||
| baa50982eb | |||
| a1e21c8bed | |||
| 719bc7000c | |||
| ed672240ec | |||
| e84f684acb | |||
|
|
ce04a2f217 | ||
| 6d32026118 | |||
| 85ab23ec7b | |||
| d736f628b2 | |||
| 8b0990869a | |||
| c8dd30ec08 | |||
| ab0c53982f | |||
| 24f5982cc2 | |||
| 17ee6f129c | |||
| dfd522206d | |||
|
|
fdc8a01037 | ||
| afeabc079e | |||
| cf53d36798 | |||
|
|
5f6bb0e6d8 | ||
| edd935d6ea | |||
| af46612a15 | |||
|
|
5443420bcb | ||
| cd5b490737 | |||
| 8e44090afe | |||
| bebac4edc5 | |||
| 5758e50cde | |||
| 0b031eaaf9 | |||
|
|
621f404a2f | ||
| 0ce7da789f | |||
| ac614bf89d | |||
|
|
9a37e74a5e | ||
| 8447ea9c6a | |||
|
|
e7f6e200bd | ||
| 1879e53fc7 | |||
|
|
89ab4f8552 | ||
| 324d914ada | |||
| 3573738cf2 | |||
|
|
062a86266e | ||
| 0ecab9db7d | |||
|
|
2bcd22cd09 | ||
| f9fe83f2f9 | |||
| 03b17b95ef | |||
| b12ceec178 | |||
| 0282b89931 | |||
| fe624be8ba | |||
| e44414e959 | |||
| 29a9011cd4 | |||
| 0017bd8e8e | |||
| ed0da29aa5 | |||
| f6dec04bf5 | |||
| 0b8d9df565 | |||
| 7e37df357d | |||
| 9c768c9092 | |||
| 5fbe862bfa | |||
| 815f19e4fe | |||
| 6300501515 | |||
| acdb676015 | |||
| cef478ed0f | |||
| dae7d2d090 | |||
| 20ae40eb69 | |||
| bfb1b89adc | |||
| 29e78e0a0b | |||
|
|
ceebcf90d4 | ||
| 5a6c097a53 | |||
| c5cb6bfe35 | |||
| dd524634fa | |||
| 9cd4e67fb6 | |||
|
|
b245ff6cf6 | ||
| d60972362c | |||
| f090119bde | |||
| 9eb0e0fac1 | |||
| 373b096598 | |||
| 06050cf62f | |||
| 78ad8f828d | |||
| 89b3b6f7c5 | |||
| 281de5f579 | |||
| 43c45f61d6 | |||
| 262c84e5da | |||
| 72994abd4d | |||
| ed80f0e5f5 | |||
| 82e3028d80 | |||
| 675f222f10 | |||
| 8c760d6062 | |||
| 9c29f3e33e | |||
| 7b0d899c09 | |||
| 27313bfa01 | |||
| cbe7658d82 | |||
| d2b7d54e30 | |||
| a55b169a1f | |||
| 69d83da439 | |||
|
|
3c5dd08e8d | ||
| 35b5226eae | |||
|
|
520ef48ce2 | ||
| 77b1238acd | |||
|
|
876253c795 | ||
| 5a9167effa | |||
|
|
7fedae8a3f | ||
| 634195a7cb | |||
| dec8200e90 | |||
| aaf1a9f1cb | |||
|
|
9a8b2ed58f | ||
| b09c30467c | |||
| f8406f42f6 | |||
|
|
952e6f4788 | ||
| 1b5e4db5d9 | |||
| c7567e569c | |||
|
|
d99ec2bcef | ||
| 8a86f6f3a6 | |||
|
|
5f507830d1 | ||
| fac4592cf7 | |||
| 4cd278ef59 | |||
|
|
7ed88d68ec | ||
| 5e3afd63fa | |||
| ad368fb418 | |||
|
|
0eac3572cd | ||
| f30724c013 | |||
| 9aaa0d8a5c | |||
|
|
783b6ae191 | ||
| 2a6d0272a2 | |||
| a0a7a6dd13 | |||
|
|
6462100737 | ||
| ff2afbab66 | |||
| 9d6e696be0 | |||
|
|
62dd3a7071 | ||
| 9f215b89f7 | |||
|
|
f09f4c3009 | ||
| cc437f698e | |||
| d9395f8ed8 | |||
|
|
9cb497ee05 | ||
| 93623009b5 | |||
| 2f28cb3f9c | |||
|
|
b1eeb5946f | ||
| 185bf9d53c | |||
| e274c9f6a2 | |||
|
|
d9d26adec4 | ||
| 21035cac27 | |||
| 48a0be3047 | |||
| 66275a6a63 | |||
| af925517ee | |||
| 08a0446c3c | |||
| 07cfd15d95 | |||
| 6afb9724be | |||
| 7f58d1ff92 | |||
| cbcd225367 | |||
| f77c31344c | |||
| 94437609a9 | |||
| b971810d21 | |||
| 0ffd67f08c | |||
| 44d4fc0ce4 | |||
| fefe15cc8e | |||
| 83a7484d8c | |||
| 2fec4ee287 | |||
|
|
eedf370bd9 | ||
| d7de84b31d | |||
| cf1b7c4dac | |||
| 7b84cc286d | |||
| 2f83c8fb48 | |||
| 017528157d | |||
| e237ca809d | |||
| e245765308 | |||
| efa6aba7d8 | |||
| 7b5149f4f3 | |||
| 55bc922829 | |||
| 96c5afb4a6 | |||
| 8dc2189f51 | |||
| d8f19ca77a | |||
| a825362538 | |||
| e050095d45 | |||
| 7d306fd135 | |||
| 8181ebe0d2 | |||
| 972270e072 | |||
| 93c5290529 | |||
| 6f2a5fa1c8 | |||
|
|
81bb9cef1c | ||
| 52830e9b1e | |||
| 2e1e33f6d0 | |||
| 998f15f51d | |||
|
|
2f8df84ae1 | ||
| 24d7977e3f | |||
| 853d3d9639 | |||
|
|
61113392ce | ||
| f8ffb667ff | |||
|
|
2b68c51685 | ||
| f0016f7894 | |||
|
|
3f22063fea | ||
| 4295514cad | |||
| d2d01d73db | |||
| 035f5a7e74 | |||
| 7175d14915 | |||
| af7a0e96a4 | |||
| 9e8cf147e9 | |||
| b829e771e6 | |||
|
|
415cb05640 | ||
| 9e409a40b0 | |||
| 5ddc09040c | |||
| f8e428da93 | |||
| 17841bbdcc | |||
| 0f069c9521 | |||
| e8039778bc | |||
| d648ce15e7 | |||
|
|
9ab89c843f | ||
| 8ee1382db5 | |||
|
|
d6aa10764a | ||
| bffce7a1b5 | |||
|
|
8a984f80be | ||
| fdda653777 | |||
|
|
f1460f63b2 | ||
| f53fda397d | |||
|
|
0cfdb138e0 | ||
| 6b2981a705 | |||
|
|
6e30c05af9 | ||
| 1cb0452e01 | |||
|
|
3e3df51c1c | ||
| 315164780b | |||
|
|
c04238d8e7 | ||
| 2aea805355 | |||
|
|
62028c52cc | ||
| 262c8bf247 | |||
|
|
246cb00362 | ||
| ee27a6da6b | |||
|
|
fc91b1af56 | ||
| 0c7bc08890 | |||
|
|
92786e3dfe | ||
| 682d207308 | |||
|
|
7fc23d080e | ||
| 4ffaf22d6f | |||
|
|
fb9e21b2fb | ||
| 24ec527b62 | |||
|
|
7f2d0deca7 | ||
| 237fc93900 | |||
|
|
571f3ac08b | ||
| 861c0b58ad | |||
|
|
119da2d5cb | ||
| d9836121cb | |||
|
|
ff554c02f0 | ||
| 71122e0c7f | |||
|
|
e59208ef0a | ||
| 3c1ae526a3 | |||
|
|
4993da6891 | ||
| 4035d3e325 | |||
|
|
30cbb30eb7 | ||
| 1319618149 | |||
|
|
35ca5647c9 | ||
| 5b8a14a6b0 | |||
|
|
7aa1e31381 | ||
| 7e60767cf1 | |||
|
|
1a86feb928 | ||
| 862f57dbb9 | |||
|
|
3c3bec2986 | ||
| e025200307 | |||
|
|
022e012e3c | ||
| 4ae66d23fa | |||
|
|
eca5a0a436 | ||
| 990108c753 | |||
|
|
a79b2abc41 | ||
| 521c2cc9df | |||
|
|
18919e3068 | ||
| 8e6a32399b | |||
|
|
d6b634708f | ||
| f2f85c3adf | |||
|
|
4cd065cee8 | ||
| bd6a047d62 | |||
|
|
7c2c9f70e2 | ||
| 93e1cd6c80 | |||
|
|
10dcc18498 | ||
| 0d34e75140 | |||
|
|
d1e58d6f98 | ||
| 877c802bf9 | |||
|
|
34af718c18 | ||
| a07be57bf9 | |||
|
|
dc680c3770 | ||
| 024bfb5007 | |||
|
|
367b28274c | ||
| a184854e90 | |||
|
|
7ec33a0dea | ||
| 26faf4631c | |||
|
|
a51bc782ea | ||
| e40e034566 | |||
|
|
ef43a66455 | ||
| a7504e4eda | |||
|
|
9ed213d4d9 | ||
| facc0b34ae | |||
|
|
e81f3a4197 | ||
| 01f993f4d4 | |||
|
|
7a3f94d6a5 | ||
| 6d92179090 | |||
|
|
5c7068dbe8 | ||
| 61fd846a24 | |||
|
|
e804459b8d | ||
| 9822734290 | |||
|
|
51387aab46 | ||
| b8ae1dc1c4 | |||
|
|
f995146249 | ||
| d6ee1ef3c5 | |||
|
|
54a4376ab5 | ||
| cb240458c9 | |||
|
|
f980fc6819 | ||
| b70bc633d3 | |||
|
|
4f0ef132eb | ||
| c945ca357f | |||
|
|
0dfe038733 | ||
| b176ff2f00 | |||
|
|
570af36ae6 | ||
| 43eb33a527 | |||
|
|
8ce2c736ca | ||
| 6bca5decbf | |||
|
|
5ae381b83b | ||
| 52fb954365 | |||
|
|
55bf74a057 | ||
| 2750e41c3a | |||
|
|
a8ba80802b | ||
| d0a972f5eb | |||
|
|
48946ce40c | ||
| 8875da759a | |||
|
|
b67bbeea06 | ||
| 010a56ae45 | |||
|
|
738be8827c | ||
| 3478881c16 | |||
|
|
89fec2beb7 | ||
| 63f0afb9de | |||
|
|
c0960a6ccb | ||
| 0cbd9ab786 | |||
|
|
20c18fb367 | ||
| ee848bd701 | |||
|
|
3c371e1379 | ||
| ea13b7000b | |||
|
|
9f31634e6e | ||
| 8f494ed90d | |||
|
|
c49a3e8996 | ||
| 8e4acdce62 | |||
|
|
346d7bcdfd | ||
| 760b02a0b7 | |||
|
|
850072390d | ||
| 10b01b9a8b | |||
|
|
5ab3128608 | ||
| 1c71e15f51 | |||
|
|
5b971d12df | ||
| e9e0acc4f5 | |||
|
|
d56e99d5e5 | ||
| 77e09a5d7a | |||
|
|
e7ee064c03 | ||
| ecea91251e | |||
|
|
23587721f3 | ||
| 4bed45f52c | |||
|
|
b943a1095e | ||
| ce20008762 | |||
|
|
070e36aaf3 | ||
| 5823429db6 | |||
|
|
306a1a8461 | ||
| 32359f3f6d | |||
|
|
eb54c445a6 | ||
| 4354f8cfe0 | |||
| 7365428f4d | |||
| 9e48580a59 | |||
| 98c181879d | |||
| 5cb6b1d84c | |||
| bcc9902ece | |||
| c018a37ba8 | |||
|
|
e49f8229b4 | ||
| 845e11d285 | |||
| bab6fa7c53 | |||
| 707e75ef66 | |||
|
|
a7fc3132dd | ||
| 851addbc54 | |||
|
|
178fba5ce5 | ||
| 791f8741f0 | |||
|
|
a38666c545 | ||
| 918c5e0255 | |||
|
|
fa4beed120 | ||
| dc4a70cfbb | |||
|
|
5ffd1abc50 | ||
| 3bd4282afe | |||
|
|
b97e2cf270 | ||
| d736c992da | |||
|
|
786a2c65d0 | ||
| 3986ad94c0 | |||
|
|
525cfc9394 | ||
| 1f4f57a371 | |||
|
|
378c3816b5 | ||
| 549a501292 | |||
|
|
1a6c059cab | ||
| 3d781e4f9a | |||
|
|
758524544c | ||
| 457dfb05e4 | |||
|
|
d10c14820b | ||
| 3e1b874918 | |||
|
|
9132a78964 | ||
| 95a4c17359 | |||
|
|
21f830efc4 | ||
| c498f0f0a8 | |||
|
|
47a2aa03dc | ||
| baab054cf1 | |||
|
|
be4b8f894f | ||
| 33feb2a952 | |||
|
|
05a9ab2a39 | ||
| 09b00b3fea | |||
|
|
691e9d3f90 | ||
| ac7b3285f6 | |||
|
|
e8f08339cd | ||
| f4a4fbc90f | |||
|
|
4b731157fa | ||
| 0ae268abb5 | |||
|
|
286c556a60 | ||
| e09c5d1198 | |||
|
|
49f5e8110e | ||
| 0e706af6ce | |||
|
|
24bc910f94 | ||
| d71ed8d884 | |||
|
|
1497d17795 | ||
| efa351982a | |||
|
|
ea793d5a36 | ||
| 6867ef13be | |||
|
|
b24ebaeb43 | ||
| c2b4f5311e | |||
|
|
ac9db489b5 | ||
| 22e47f0575 | |||
|
|
6e0878671f | ||
| 86107751fd | |||
|
|
9377e1b3c8 | ||
| 6daa8cf602 | |||
|
|
4535b3df6b | ||
| 5768e5c861 | |||
|
|
00a1654250 | ||
| 380e71888c | |||
| 86c981c640 | |||
| db28eb0941 | |||
|
|
2e89c8eff3 | ||
| 0ad818a240 | |||
|
|
42772d64e3 | ||
| f61562e922 | |||
|
|
fc388113f9 | ||
| 4af3ac4d8e | |||
|
|
6666f462e4 | ||
| 1806965389 | |||
|
|
6013a92fba | ||
| 2e7ba44f35 | |||
|
|
8daab28ac9 | ||
| 1e342eafb4 | |||
|
|
21b8181050 | ||
| ecc54c40cd | |||
|
|
87b606a917 | ||
| 35c57e99ca | |||
|
|
6361474a11 | ||
| ce518d6848 | |||
|
|
6cda96886a | ||
| fee17b9bfd | |||
|
|
9487d40a6d | ||
| b20bf2dcb3 | |||
|
|
3f76453cf4 | ||
| b207c32e39 | |||
|
|
fbeb524f35 | ||
| e26705c068 | |||
|
|
6960ab22f5 | ||
| 00ddd49eca | |||
|
|
a6dd41af8a | ||
| e5fcc0db74 | |||
|
|
40b8aca4e8 | ||
| cd4253aac4 | |||
|
|
0185732f92 | ||
| 757853ef1a | |||
|
|
0e0cdef12a | ||
| 95898062f1 | |||
|
|
6e6ff34ab0 | ||
| a05ce90652 | |||
|
|
25968a628f | ||
| a58d6dccfc | |||
|
|
540e60e912 | ||
| 8ae82ae213 | |||
| 665ba9282d | |||
|
|
7bb483a4c0 | ||
| c3649f8b98 | |||
|
|
87a7c3c298 | ||
| f554cbc545 | |||
|
|
1f6d3ce740 | ||
| dfa7160a90 | |||
|
|
9ff5426c84 | ||
| e6053feda6 | |||
|
|
aa9053b5da | ||
| 6105aa51ce | |||
|
|
53ef09892b | ||
| 87d2f0bbb8 | |||
|
|
5e83236f21 | ||
| 0fdea768f3 | |||
|
|
056cbb1a67 | ||
| 31fa2a055b | |||
|
|
4733cbdc9e | ||
| c635e7f7ad | |||
|
|
c57e7ccdc6 | ||
| 68fb650340 | |||
|
|
133c9a329f | ||
| 0471d4e051 | |||
|
|
a7cac1b3a6 | ||
| b3d83bc924 | |||
|
|
51ecb545f4 | ||
| 8665d897a4 | |||
|
|
c7627f6a02 | ||
| c8cd19c480 | |||
|
|
a2f6e7c7fa | ||
| 48fbb6acc4 | |||
| 0f4b955696 | |||
|
|
ee8696c717 | ||
| 93b2635042 | |||
|
|
fde37801d3 | ||
| 6baf5b9f27 | |||
|
|
bfc7315f84 | ||
| df529dccd3 | |||
| 5062b3ee9f | |||
|
|
0c3e64b701 | ||
| 2c5c2b2fe2 | |||
| 2723d1ceec | |||
|
|
72c883f436 | ||
| efdf87b4de | |||
|
|
affebb0317 | ||
| 54403b1a5a | |||
|
|
bf78ea38e2 | ||
| 300514d2d8 | |||
|
|
713e312b0c | ||
| 3c010a3682 | |||
|
|
5c5047222c | ||
| 669e2886f9 | |||
|
|
13563a28b9 | ||
| 4a944312a7 | |||
|
|
bfa15a65b9 | ||
| e12ce0ec69 | |||
|
|
341f988d48 | ||
| e186993dcd | |||
|
|
3458781be8 | ||
| 8fcd762824 | |||
|
|
6bf1c53c5f | ||
| 21da2d7e5a | |||
| 4b3190a5fb | |||
|
|
5898529f93 | ||
| 94616cb651 | |||
|
|
21057cdc64 | ||
| 467fca52eb | |||
|
|
c898966bda | ||
| 595226928e | |||
|
|
ff34ce543a | ||
| 55b3b4b2de | |||
|
|
73aac5f11d | ||
| 3c3c6d3d5e | |||
|
|
5aba8ec270 | ||
| b3f735fb24 | |||
|
|
11b9e2f66a | ||
| 7029e755ce | |||
|
|
cef7b49b55 | ||
| cd78b68d02 | |||
|
|
f64eeb5a79 | ||
| 700d66553b | |||
|
|
496eb3beb4 | ||
| a868950eb5 | |||
|
|
3e3b092174 | ||
| 3e090a1198 | |||
|
|
3747ad8138 | ||
| f27a8920da | |||
|
|
b5003151d8 | ||
| 27ee1f9673 | |||
|
|
fb3fbc78ba | ||
| edc2138bfc | |||
|
|
29cd39d25e | ||
| 64f515f8cc | |||
| 943a757d7a | |||
|
|
0cce7eab45 | ||
| 351163f705 | |||
| a996f06914 | |||
|
|
6a9da0d098 | ||
| da0e51f238 | |||
|
|
eab8badfd7 | ||
| ed56b7c626 | |||
|
|
b623d41a10 | ||
| 69d56570b3 | |||
|
|
a845f2cb11 | ||
| f8faa0bfce | |||
|
|
710f13b075 | ||
| 0f8789bfa8 | |||
|
|
f492581431 | ||
| d29a3adb67 | |||
|
|
7d3f1e488e | ||
| 131ed83433 | |||
|
|
69b9e22e27 | ||
| b0a42358fa | |||
|
|
e9d9b7ff10 | ||
| 5f3f9dce1c | |||
|
|
44a4da3f9a | ||
| 13c20ca33f | |||
|
|
07e4f2a39a | ||
| e2d03753af | |||
|
|
aff03faeb8 | ||
| 9e863c3d72 | |||
|
|
84901142e5 | ||
| 2fa63b8874 | |||
| e2a2500246 | |||
|
|
0d26c79b20 | ||
| e607dbe7de | |||
|
|
df63fad61c | ||
| 6cbef0179e | |||
|
|
30562aa0ec | ||
| 4a23bbd2b8 | |||
|
|
3c850f1409 | ||
| b09473809c | |||
|
|
8fc2fca92b | ||
| 7bd87acee2 | |||
|
|
4efc9b292e | ||
| 965c9609af | |||
|
|
70855f09b6 | ||
| 45512fd022 | |||
|
|
b17a74c4ff | ||
| 810781a235 | |||
|
|
ff3f434f3f | ||
| dd53efb6d3 | |||
|
|
cb86ec3ac4 | ||
| 269789518a | |||
|
|
d0a0aac114 | ||
| 70442718f7 | |||
|
|
7625bd5b77 | ||
| 78ed110d4b | |||
|
|
2eed69e4d1 | ||
| 0c91e3769a | |||
|
|
ef6523d215 | ||
| 113b674dbe | |||
|
|
36e954b8db | ||
| 71279d8c05 | |||
|
|
7058c175a2 | ||
| 81b8f4c464 | |||
|
|
e63c490fd8 | ||
| 8cb08ff676 | |||
|
|
bfface3513 | ||
| 1186269f2a | |||
|
|
825ee4a600 | ||
| 1fc72f4617 | |||
|
|
a7d0c318be | ||
| 1905a281e9 | |||
|
|
570b6e02a5 | ||
| 7140e48938 | |||
|
|
b061d51d14 | ||
| b0cffafde7 | |||
|
|
716ff6979f | ||
| c7fb9ad271 | |||
|
|
5320b7003a | ||
| 2ab5eb76eb | |||
|
|
de8e35d3fa | ||
| b11f302a7d | |||
|
|
5f019aa1b6 | ||
| a8efbaf8c8 | |||
|
|
4062d82019 | ||
| 0d2a1607aa | |||
|
|
d6e22765ea | ||
| be42df9f8f | |||
|
|
ef13a9d068 | ||
| 0bae18940f | |||
|
|
eae0a46482 | ||
| ab2c659d14 | |||
|
|
07668450ad | ||
| 840884057a | |||
|
|
c9c44c73ac | ||
| 98146307ed | |||
|
|
944ccf4460 | ||
| 0da4e0c7e8 | |||
|
|
c2bd968c23 | ||
| e88c7ea6d6 | |||
|
|
14fad13d33 | ||
| b64f5e8c2c | |||
|
|
4c191fd951 | ||
| 761c219fa3 | |||
|
|
1887fc6424 | ||
| 30977362d7 | |||
|
|
b581833526 | ||
| 6a335080f0 | |||
|
|
a0204f50ad | ||
| 09174d084d | |||
|
|
1cea59ac56 | ||
| c8da9b4b99 | |||
|
|
d3445a68fd | ||
| 63144ed73b | |||
|
|
4275d84b66 | ||
| 4a5db62031 | |||
|
|
acf78dc27e | ||
| 22af95a6bd | |||
|
|
ce3ded2bfe | ||
| 222d7351dd | |||
|
|
81e6c54e65 | ||
| 15db95dac4 | |||
|
|
cef154264f | ||
| d1bfc519dd | |||
|
|
9087429c20 | ||
| 1f8464d167 | |||
|
|
95add2f31e | ||
| fa3a0e7dfb | |||
|
|
c4903ad90a | ||
| 8a42af0069 | |||
|
|
46bf87162c | ||
| a0c0d3f631 | |||
|
|
2102cbac8c | ||
| cc323fde22 | |||
|
|
08a6654b24 | ||
| fa54c47de1 | |||
| 0c553ab8e4 | |||
| 6c3e1a8593 | |||
|
|
1c4fc21211 | ||
| 3f6d5417ba | |||
|
|
3ea296e164 | ||
| c29ef507d0 | |||
|
|
43cf956c04 | ||
| 49b56a2ea5 | |||
| 70a6590d4e | |||
|
|
8fd5dac6c4 | ||
| 466856ed98 | |||
| 84d3019f17 | |||
|
|
233b805698 | ||
| d38409f677 | |||
|
|
0c14aa1106 | ||
| a9f9df7640 | |||
|
|
220b060ce1 | ||
| 5a292c09ab | |||
|
|
3944528dbe | ||
| 012b681130 | |||
|
|
c8db004298 | ||
| 41061aa89f | |||
|
|
af9b202e4a | ||
| a8a78d605d | |||
|
|
d9f7456dcb | ||
| e93da18b59 | |||
|
|
4a711640b9 | ||
| d3b9c6e9c3 | |||
|
|
63d672e90e | ||
| ee6ee3b8e8 | |||
|
|
24070153f3 | ||
| e3e61c0658 | |||
|
|
4942774380 | ||
| dd2e93e7ee | |||
|
|
8ad72a9669 | ||
| 0b44807851 | |||
|
|
f08f805b1d | ||
| 930d79d579 | |||
| 141f344654 | |||
| ae324d389e | |||
| 70b5bb8fa4 | |||
| 43b6ae4f36 | |||
|
|
87b0552db1 | ||
| 31ac3af78d | |||
|
|
aa77d17c7b | ||
| a277af867c | |||
|
|
3285c43e27 | ||
| 49c416a3fb | |||
| 8f0902e4e3 | |||
| 8dd87f7182 | |||
| c1945d875d | |||
| ba38ea0f92 | |||
| 887f69f018 | |||
| 1fb7c08c23 | |||
| b333ee2b14 | |||
| c0be7ceb59 | |||
| dd6d0c8bd6 | |||
| 288a56f973 | |||
| f2f8380438 | |||
| 8b71af57c0 | |||
| 57d9459ffa | |||
| 5839c11257 | |||
| c63fc09b39 | |||
| e78f20149e | |||
| 3039605a18 | |||
| 27773a6ae1 | |||
| 3aec17a37b | |||
|
|
560bf535f5 | ||
| c44716a16b | |||
| aa29763b7e | |||
|
|
c9dd7ccdd5 | ||
| 76814d1d15 | |||
| 49fd7cbbb3 | |||
|
|
bdb1392f58 | ||
| 76a2376b61 | |||
|
|
77f1882f9c | ||
| 8e35f164b7 | |||
|
|
e8f21bbfe6 | ||
| d8c9c54d6a | |||
|
|
2ca162ebba | ||
| f2288bedb1 | |||
|
|
4d339c81ac | ||
| 4f669d2969 | |||
|
|
b72c1ab3be | ||
| 7e803b4704 | |||
|
|
b2493b571f | ||
| 78adacb9ce | |||
|
|
25ce1607a5 | ||
| 09fbe40968 | |||
|
|
561e932612 | ||
| e5eb4b8136 | |||
|
|
7d275d7544 | ||
| 6ac6644e9c | |||
|
|
f493f56e39 | ||
| 2c96082d72 | |||
|
|
60a6917001 | ||
| 7d311abc16 | |||
|
|
6039807b88 | ||
| efa9b75f94 | |||
|
|
4ccbcd6f85 | ||
| f7569f0383 | |||
|
|
3127fddcaa | ||
| 0cc7e4d8d5 | |||
|
|
b04318f970 | ||
| 22f0474dc9 | |||
| 0a7e92c153 | |||
|
|
d87225f8e9 | ||
| ca16814ede | |||
|
|
4416c9e563 | ||
| 469a7d5d07 | |||
|
|
e2842d573f | ||
| a7de4efbc5 | |||
|
|
492a507e58 | ||
| 0a599af581 | |||
|
|
75fc17c894 | ||
| 98158e4174 | |||
|
|
57f371fc72 | ||
| 38ee18f66a | |||
| 91f081b92f | |||
|
|
3eb2fac0a2 | ||
| da4a73364d | |||
|
|
d64a56b5c3 | ||
| 42c0dfc1df | |||
| a74ed76257 | |||
| 8cee9cea26 | |||
| 3f5807d041 | |||
| 21294bc828 | |||
|
|
c21058ae02 | ||
| db098c9d03 | |||
|
|
2f56b9b840 | ||
| 9f2b88afa5 | |||
| 357cbcd82e | |||
| 1a8678544b | |||
| db5bd6ab58 | |||
|
|
c147d5d6ed | ||
| dd6e91681c | |||
|
|
8c0b845a71 | ||
| 52fcee4b12 | |||
| 2ddbb21a96 | |||
|
|
abbb39627b | ||
| 682236ce65 | |||
|
|
65575be40f | ||
| 729e3cc83e | |||
| 50951dda69 | |||
|
|
158f29c20f | ||
| 6481bcc9ae | |||
|
|
f41b0847f5 | ||
| c567a9ed20 | |||
| ec8cc58c40 | |||
| 722ca999cf | |||
|
|
fb0c67f0d5 | ||
| 78becf3fed | |||
| 54334f8f1c | |||
|
|
1626e51961 | ||
| b79868f03a | |||
|
|
2fdc603253 | ||
| 653fafaf60 | |||
| 529342d9be | |||
|
|
8dbca264f4 | ||
| 309d8a6dc5 | |||
| 2531472044 | |||
|
|
3f200f9cdd | ||
| c3da588d77 | |||
| 31fa9103ad | |||
|
|
c521c739cf | ||
| 1252a9a4cc | |||
| 3eba893081 | |||
|
|
cc38fd7dcf | ||
| 8fee072532 | |||
| babbbff748 | |||
|
|
5f401ac27a | ||
| 7bedd3b0bf | |||
| c85afe59f9 | |||
|
|
1fcd387db1 | ||
| cdee895987 | |||
|
|
c3befc334d | ||
| d7cae9fc33 | |||
|
|
eb16ce37d1 | ||
| a8b702b833 | |||
|
|
714eec246d | ||
| 8364e33b6e | |||
|
|
2cec196cae | ||
| 8d68dfe207 | |||
|
|
dfb4a3fa2c | ||
| b8d972c95f | |||
|
|
5269bd756f | ||
| b6e1a5c435 | |||
|
|
013b7264b0 | ||
| 66a84b7335 | |||
|
|
70ffd6fded | ||
| bf92073872 | |||
| c9bf747432 | |||
| 37c52ef49a | |||
|
|
1a3870ff4f | ||
| 84f2946e6b | |||
| 0fa4d0229a | |||
|
|
4cb0f31c97 | ||
| 9b442b62bc | |||
| e75b1b314a | |||
| 1d4f473492 | |||
| aad33e4e35 | |||
| 9e7b7b7b4c | |||
| f7222d8ee3 | |||
| f9728ffed6 | |||
| 388ddf5bf2 | |||
|
|
9089ee9066 | ||
| e64819ec07 | |||
| 420e8a7348 | |||
| dc53a63200 | |||
|
|
b5cc565abb | ||
| 3d7ff9b73f | |||
| 68607f36d4 | |||
|
|
4dc1328815 | ||
| 440778760a | |||
|
|
fa66c5e141 | ||
| 66c40b75fc | |||
|
|
4a3a4df794 | ||
| 7b8b417e0d | |||
|
|
b2d8d22eb3 | ||
| e8e95982ab | |||
| 7369da424b | |||
| 6aff998447 | |||
|
|
bb2d165cca | ||
| 80eb31bf79 | |||
|
|
ef93bb02fe | ||
| e6085978cf | |||
|
|
629a95b532 | ||
| 4410c8c021 | |||
|
|
80d177fffd | ||
| a9193274bd | |||
| 2b971d4fb7 | |||
| 1914fa8717 | |||
|
|
2cb0eca931 | ||
| 88bb14a1e4 | |||
|
|
3686d53593 | ||
| 2829f009e2 | |||
|
|
4df3af7d31 | ||
| 3dd197e47d | |||
| 65820d3957 | |||
| 92143479ce | |||
| 96c1481b8d | |||
|
|
5cb039378c | ||
| 386b040029 | |||
|
|
ea692fe4d6 | ||
| 338e86999c | |||
| ea065e3103 | |||
|
|
cc818de479 | ||
| dbd96c7c32 | |||
|
|
e3f25df854 | ||
| b006f107e0 | |||
| 806ec40240 | |||
|
|
f0373df54a | ||
| 9b4191cc73 | |||
| 51978dd809 | |||
|
|
6d150992b8 | ||
| cfa4bd667a | |||
| 46bf61a3ef | |||
|
|
49478b9969 | ||
| b2573425f0 | |||
| df61d4d1c2 | |||
| 20cd6196ff | |||
|
|
bb1ca3b5f7 | ||
| dff1b20d40 |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
custom: https://paypal.me/planetrenox
|
||||||
26
.github/workflows/BUILD.yml
vendored
@@ -1,23 +1,11 @@
|
|||||||
on:
|
on: push
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
permissions: write-all
|
|
||||||
jobs:
|
jobs:
|
||||||
build-push:
|
build-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
- uses: oven-sh/setup-bun@v2
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
- run: bun install && bun run build
|
||||||
- uses: actions/setup-node@v4
|
- run: git config user.name github-actions[bot] && git config user.email github-actions[bot]@users.noreply.github.com && git add . && git commit -m "This build was committed by a bot." && git push
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
- run: |
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
- run: |
|
|
||||||
git config user.name "github-actions"
|
|
||||||
git config user.email "github-actions@github.com"
|
|
||||||
git add .
|
|
||||||
git commit -m "This build was committed by a bot."
|
|
||||||
git push
|
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -8,7 +8,6 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
@@ -24,3 +23,4 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
bun.lock
|
||||||
|
|||||||
31
README.md
@@ -1,31 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
```
|
|
||||||
v0.3: added dedup threads functionality
|
|
||||||
v0.4: mobile keyboard bug fixes
|
|
||||||
v0.5: code highlighting only after stream
|
|
||||||
v0.6: aborting mid stream fix
|
|
||||||
v0.7: correctly remember assistant name & model
|
|
||||||
v0.8: introducing sunes
|
|
||||||
v0.9: htmx
|
|
||||||
v0.10: sune pin, rename, and pfp
|
|
||||||
v0.11: top center avatar match sune pfp
|
|
||||||
v0.12: ✺
|
|
||||||
v0.13: lucide sparkles & top_p
|
|
||||||
v0.14: vite-plugin-pwa
|
|
||||||
v0.15.1: android apk first release
|
|
||||||
v0.16: icons & removed bad tips
|
|
||||||
v0.17: reasoning effort setting
|
|
||||||
v0.18: touch ripple effect
|
|
||||||
v0.19: import/export keeps last updated
|
|
||||||
v0.20: token counting
|
|
||||||
v0.21: restyled input ui
|
|
||||||
v0.22: jpg, webp, png, gif, pdf, mp3, wav
|
|
||||||
v0.23: attachment badge bugs
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The issues tab in this repo isn't only for issues. You can open an issue to just say Hi.
|
|
||||||
BIN
dist/appstore_content/latex.png
vendored
Normal file
|
After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
BIN
dist/appstore_content/screenshot2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
BIN
dist/appstore_content/screenshot4.jpg
vendored
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
dist/appstore_content/screenshot5.jpg
vendored
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
dist/appstore_content/screenshot6.jpg
vendored
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
dist/appstore_content/screenshot_marketplace.jpg
vendored
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
dist/appstore_content/screenshot_miku.png
vendored
Normal file
|
After Width: | Height: | Size: 805 KiB |
BIN
dist/appstore_content/sync.png
vendored
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
32
dist/assets/index-CLEI5Rwr.css
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@import url(https://fonts.bunny.net/css?family=assistant:500);
|
||||||
|
:root{--safe-bottom:env(safe-area-inset-bottom)}
|
||||||
|
::-webkit-scrollbar{height:8px;width:8px}
|
||||||
|
::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}
|
||||||
|
.no-scrollbar::-webkit-scrollbar{display:none}
|
||||||
|
.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
|
||||||
|
html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif}
|
||||||
|
.markdown-body{font-size:14px;line-height:1.6}
|
||||||
|
.markdown-body pre{overflow:auto}
|
||||||
|
.markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em}
|
||||||
|
.msg-bubble{overflow-x:auto}
|
||||||
|
.msg-avatar{font-size:16px}
|
||||||
|
.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
|
||||||
|
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:0.875rem;display:flex;align-items:center;gap:.5rem}
|
||||||
|
#htmlEditor,#extensionHtmlEditor,#jsonSchemaEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5;}
|
||||||
|
:not(pre)>code{font-size:85%;padding:.2em .4em;margin:0;border-radius:6px;background-color:rgba(175,184,193,0.2)}
|
||||||
|
#threadRepoInput::placeholder{font-family:sans-serif;font-weight:500;color:#9ca3af}
|
||||||
|
/* MathJax 3 SVG Scaling & Alignment */
|
||||||
|
mjx-container[jax="SVG"] {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 0.125em !important;
|
||||||
|
}
|
||||||
|
mjx-container[jax="SVG"][display="true"] {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em 0 !important;
|
||||||
|
}
|
||||||
|
mjx-container svg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
2390
dist/assets/index-DqaW9pQA.js
vendored
Normal file
201
dist/index.html
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>Sune</title>
|
||||||
|
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css"/>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js"></script>
|
||||||
|
<script defer src="//unpkg.com/alpinejs"></script>
|
||||||
|
<script defer src="https://c.planetrenox.com/tracker.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="module" crossorigin src="/assets/index-DqaW9pQA.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-CLEI5Rwr.css">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
||||||
|
<body class="bg-white text-gray-900 selection:bg-black/10" x-data @click.window="if($event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains($event.target)&&!$event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains($event.target)&&!$event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains($event.target)&&!document.getElementById('userMenuBtn').contains($event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
||||||
|
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
|
||||||
|
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||||
|
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
||||||
|
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
||||||
|
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
|
||||||
|
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-0 border-b border-gray-200 hidden"></section><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" @click="if($event.target.closest('.msg-avatar')){document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')}"></div><div class="h-24"></div></main>
|
||||||
|
|
||||||
|
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
||||||
|
<div class="mx-auto w-full max-w-none px-0">
|
||||||
|
<form id="composer" class="group relative flex items-start gap-2 px-3">
|
||||||
|
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
|
||||||
|
<div class="flex flex-col gap-2 self-stretch justify-center">
|
||||||
|
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
|
||||||
|
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
|
||||||
|
</div>
|
||||||
|
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
|
||||||
|
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
|
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
||||||
|
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
|
<div class="p-3 border-t relative">
|
||||||
|
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" @click.stop="document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
|
||||||
|
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
|
||||||
|
<button id="accountSettingsOption" class="menu-item"><i data-lucide="settings" class="h-4 w-4"></i><span>Settings</span></button>
|
||||||
|
<button id="sunesImportOption" class="menu-item">Import sunes (.sune)</button>
|
||||||
|
<button id="sunesExportOption" class="menu-item">Export sunes (.sune)</button>
|
||||||
|
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
|
||||||
|
</div>
|
||||||
|
<input id="importInput" type="file" accept="application/json,.json,.sune" class="hidden"/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div id="sidebarOverlayRight" class="fixed inset-0 z-40 bg-black/20 hidden" @click="$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"></div>
|
||||||
|
<aside id="sidebarRight" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
|
<div class="p-2 border-b flex flex-col gap-2">
|
||||||
|
<input id="threadRepoInput" type="text" placeholder="gh://owner/repo" class="w-full h-9 rounded-lg border-0 bg-gray-100 px-3 text-xs font-mono focus:ring-2 focus:ring-black focus:bg-white"/>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button id="threadBackBtn" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center hidden" title="Back"><i data-lucide="chevron-left" class="h-4 w-4"></i></button>
|
||||||
|
<button id="threadFolderBtn" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center hidden" title="New Folder"><i data-lucide="folder-plus" class="h-4 w-4"></i></button>
|
||||||
|
</div>
|
||||||
|
<button id="threadSyncBtn" class="px-3 py-1 rounded-lg bg-black text-white text-[10px] font-bold uppercase tracking-wider hover:bg-black/90 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="threadList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="threadPopover" class="menu-card hidden">
|
||||||
|
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
||||||
|
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
||||||
|
<button data-action="duplicate" class="menu-item"><i data-lucide="copy" class="h-4 w-4"></i><span>Duplicate</span></button>
|
||||||
|
<button data-action="copy_path" class="menu-item"><i data-lucide="link" class="h-4 w-4"></i><span>Copy Path (GH)</span></button>
|
||||||
|
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export thread (.json)</span></button>
|
||||||
|
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
|
||||||
|
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
|
||||||
|
</div>
|
||||||
|
<div id="sunePopover" class="menu-card hidden">
|
||||||
|
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
||||||
|
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
||||||
|
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
|
||||||
|
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
|
||||||
|
</div>
|
||||||
|
<div id="suneModal" class="hidden fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/30"></div>
|
||||||
|
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
|
||||||
|
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center gap-2"><input id="suneURL" type="text" placeholder="" class="flex-1 min-w-0 h-10 rounded-xl border-0 bg-gray-50 px-3 text-gray-400 placeholder:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:bg-white text-xs font-mono focus:text-black"/><button id="syncSune" class="p-1.5 rounded hover:bg-gray-100" aria-label="Refresh"><i data-lucide="refresh-cw" class="h-5 w-5"></i></button></div>
|
||||||
|
<form id="settingsForm" class="text-sm">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
|
||||||
|
<div id="panelModel" class="p-4 space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top K</label><input id="set_top_k" type="number" min="0" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Frequency Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_frequency_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-2">
|
||||||
|
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
|
||||||
|
<div><input id="set_json_output" type="checkbox" class="sr-only peer"><label for="set_json_output" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">JSON Output</label></div>
|
||||||
|
<div><input id="set_img_output" type="checkbox" class="sr-only peer"><label for="set_img_output" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">IMG Output</label></div>
|
||||||
|
<div><input id="set_hide_composer" type="checkbox" class="sr-only peer"><label for="set_hide_composer" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Hide composer</label></div>
|
||||||
|
<div><input id="set_ignore_master_prompt" type="checkbox" class="sr-only peer"><label for="set_ignore_master_prompt" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Ignore master prompt</label></div>
|
||||||
|
</div>
|
||||||
|
<div id="aspectRatioContainer" class="hidden pt-2 grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-medium mb-1 text-xs">Aspect Ratio</label>
|
||||||
|
<select id="set_aspect_ratio" class="w-full rounded-xl border border-gray-300 px-3 py-2 text-xs">
|
||||||
|
<option value="1:1">1:1 (Square)</option>
|
||||||
|
<option value="2:3">2:3 (Portrait)</option>
|
||||||
|
<option value="3:2">3:2 (Landscape)</option>
|
||||||
|
<option value="3:4">3:4</option>
|
||||||
|
<option value="4:3">4:3</option>
|
||||||
|
<option value="4:5">4:5</option>
|
||||||
|
<option value="5:4">5:4</option>
|
||||||
|
<option value="9:16">9:16 (Story)</option>
|
||||||
|
<option value="16:9">16:9 (Cinematic)</option>
|
||||||
|
<option value="21:9">21:9 (Ultra-wide)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-medium mb-1 text-xs">Resolution</label>
|
||||||
|
<select id="set_image_size" class="w-full rounded-xl border border-gray-300 px-3 py-2 text-xs">
|
||||||
|
<option value="1K">1K (Standard)</option>
|
||||||
|
<option value="2K">2K (High)</option>
|
||||||
|
<option value="4K">4K (Ultra)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="panelPrompt" class="p-4 space-y-4 hidden">
|
||||||
|
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">JSON Schema</label><pre id="jsonSchemaEditor" class="w-full h-48 p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre><p class="mt-1 text-xs text-gray-500">Requires "JSON Output" to be enabled. Value for <code>json_schema</code>.</p></div>
|
||||||
|
</div>
|
||||||
|
<div id="panelScript" class="p-1 hidden">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></button></div>
|
||||||
|
<div class="pt-0">
|
||||||
|
<pre id="htmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
|
||||||
|
<pre id="extensionHtmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5 hidden" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex gap-2"><button type="button" id="copyHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Paste</button></div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Scripts also run. extension.html runs before index.html.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
||||||
|
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button> <div class="flex items-center justify-end gap-2"><button type="button" id="cancelSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/30"></div>
|
||||||
|
<div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4">
|
||||||
|
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
||||||
|
<form id="accountSettingsForm" class="text-sm">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="accountTabGeneral" class="flex-1 py-2 px-3 text-center border-b-2 border-black">General</button><button type="button" id="accountTabAPI" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">API</button><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div>
|
||||||
|
<div id="accountPanelGeneral" class="p-4 space-y-4">
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: to override.</p></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
|
||||||
|
</div>
|
||||||
|
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div><div><label class="block text-gray-700 font-medium mb-1">GCP Service Acct</label><input id="gcpSAInput" type="file" class="hidden" accept="application/json,.json"><button type="button" id="gcpSAUploadBtn" class="w-full text-left rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm hover:bg-gray-50 truncate">Upload .json</button><p class="mt-1 text-xs text-gray-500">Use: <code>USER.gcpSA</code></p></div></div></div>
|
||||||
|
<div id="accountPanelUser" class="p-4 space-y-4 hidden">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>
|
||||||
|
<input id="userAvatarInput" type="file" accept="image/*" class="hidden">
|
||||||
|
<div class="flex-1"><label for="set_user_name" class="block text-gray-700 font-medium mb-1">Username</label><input id="set_user_name" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Master"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
||||||
|
<div class="flex items-center gap-2"><button type="button" id="importAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Import</button><button type="button" id="exportAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Export</button></div>
|
||||||
|
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="importAccountSettingsInput" type="file" class="hidden" accept="application/json,.json">
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
docs/registerSW.js → dist/registerSW.js
vendored
1
dist/sw.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const l=e=>i(e,t),c={module:{uri:t},exports:o,require:l};s[t]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(r(...e),o))}}define(["./workbox-8c29f6e4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"a2b4e0b2e3e9dcaa81467eafab97eae7"},{url:"assets/index-DqaW9pQA.js",revision:null},{url:"assets/index-CLEI5Rwr.css",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||||
0
docs/✺.avif → dist/✺.avif
vendored
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
BIN
dist/✺.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
sune.planetrenox.com
|
|
||||||
|
Before Width: | Height: | Size: 134 KiB |
198
docs/index.html
@@ -1,198 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
|
||||||
<title>Sune</title>
|
|
||||||
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif">
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css"/>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"/>
|
|
||||||
<style>
|
|
||||||
:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
|
|
||||||
.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
|
|
||||||
.msg-bubble{overflow-x:auto}
|
|
||||||
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:#0f172a;color:#fff;border-radius:.5rem;padding:.25rem .5rem;font-size:12px;opacity:.85}
|
|
||||||
.copy-btn:hover{opacity:1}
|
|
||||||
.msg-avatar{font-size:16px}
|
|
||||||
.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
|
|
||||||
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:.875rem;display:flex;align-items:center;gap:.5rem}
|
|
||||||
.menu-item:hover{background:#f9fafb}
|
|
||||||
</style>
|
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
|
||||||
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
|
||||||
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(!document.getElementById('historyMenu').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideHistoryMenu(); if(!document.getElementById('suneMenu').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSuneMenu(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
|
||||||
<div class="flex flex-col h-dvh max-h-dvh">
|
|
||||||
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
|
||||||
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
|
||||||
<button id="sidebarBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" hx-on="click:document.getElementById('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
|
||||||
<button id="settingsBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
|
|
||||||
<div class="justify-self-end"><button id="historyBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" hx-on="click:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" hx-on="click: if(event.target.closest('.msg-avatar')){document.getElementById('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('hidden')}"></div><div class="h-24"></div></main>
|
|
||||||
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
|
||||||
<div class="mx-auto w-full max-w-none px-0">
|
|
||||||
<form id="composer" class="group relative flex items-start gap-2 px-3">
|
|
||||||
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
|
|
||||||
<div class="flex flex-col gap-2 self-stretch justify-center">
|
|
||||||
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
|
|
||||||
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
|
|
||||||
</div>
|
|
||||||
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<div id="sidebarOverlay" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:document.getElementById('sidebar').classList.add('-translate-x-full');this.classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full');document.getElementById('historyOverlay').classList.add('hidden');hideHistoryMenu();hideSuneMenu()"></div>
|
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
|
||||||
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
|
||||||
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
|
|
||||||
<div class="p-3 border-t relative">
|
|
||||||
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" hx-on="click:event.stopPropagation();document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
|
|
||||||
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
|
|
||||||
<button id="apiKeyOption" class="menu-item">Enter OpenRouter API key</button>
|
|
||||||
<button id="sunesImportOption" class="menu-item">Import sunes (.json)</button>
|
|
||||||
<button id="sunesExportOption" class="menu-item">Export sunes (.json)</button>
|
|
||||||
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
|
|
||||||
<button id="threadsExportOption" class="menu-item">Export threads (.json)</button>
|
|
||||||
</div>
|
|
||||||
<input id="importInput" type="file" accept="application/json,.json" class="hidden"/>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div id="historyOverlay" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:this.classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full')"></div>
|
|
||||||
<aside id="historyPanel" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
|
||||||
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>Threads</span><button id="closeHistory" class="p-1 rounded hover:bg-gray-100" aria-label="Close" hx-on="click:document.getElementById('historyOverlay').classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full')"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
|
||||||
<div id="historyList" class="flex-1 overflow-y-auto divide-y"></div>
|
|
||||||
</aside>
|
|
||||||
<div id="historyMenu" class="menu-card hidden">
|
|
||||||
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
|
||||||
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
|
||||||
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
|
|
||||||
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
|
|
||||||
</div>
|
|
||||||
<div id="suneMenu" class="menu-card hidden">
|
|
||||||
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
|
||||||
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
|
||||||
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
|
|
||||||
</div>
|
|
||||||
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
|
||||||
<div class="absolute inset-0 bg-black/30"></div>
|
|
||||||
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
|
|
||||||
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Sune Settings</span><button id="closeSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
|
||||||
<form id="settingsForm" class="text-sm">
|
|
||||||
<div class="border-b flex text-sm font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button></div>
|
|
||||||
<div id="panelModel" class="p-4 space-y-4">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="openai/gpt-4o"/></div>
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Variety. Lower = predictable.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Nucleus sampling.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top K</label><input id="set_top_k" type="number" min="0" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0"/><p class="mt-1 text-xs text-gray-500">Token shortlist size.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Frequency Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_frequency_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage repeats by count.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Presence Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_presence_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage seen tokens.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Reduce verbatim echoes.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Minimum token prob vs best.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Adaptive nucleus filter.</p></div>
|
|
||||||
</div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Default</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">Used only if supported by the model. (Default = Omitted)</p></div>
|
|
||||||
</div>
|
|
||||||
<div id="panelPrompt" class="p-4 space-y-4 hidden">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black/20" placeholder="Enter a system prompt to guide the sune"></textarea><p class="mt-1 text-xs text-gray-500">Saved per sune.</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
|
||||||
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button>
|
|
||||||
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
|
||||||
<script>
|
|
||||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
|
||||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','panelModel','panelPrompt','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput'].map(id=>[id,document.getElementById(id)]))
|
|
||||||
const icons=()=>window.lucide&&lucide.createIcons()
|
|
||||||
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c]))
|
|
||||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}}
|
|
||||||
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
|
||||||
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,reasoning_effort:'default',system_prompt:''}
|
|
||||||
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
|
|
||||||
let sunes=(su.load()||[]).map(makeSune)
|
|
||||||
if(!sunes.length){const def=makeSune({name:'Default'});sunes=[def];su.save(sunes);su.setActiveId(def.id)}
|
|
||||||
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>makeSune({name:'Default'})
|
|
||||||
const store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;const a=getActiveSune();if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='apiKey'){globalStore.apiKey=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}})
|
|
||||||
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[]}
|
|
||||||
const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm}
|
|
||||||
const reflectActiveSune=()=>{const a=getActiveSune();el.settingsBtnTop.title=`Settings — ${a.name}`;el.settingsBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';icons()}
|
|
||||||
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===su.getActiveId()?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-6 w-6 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
|
|
||||||
const renderSidebar=()=>{const list=[...sunes].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
|
|
||||||
function enhanceCodeBlocks(root,doHL=true){root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn';btn.textContent='Copy';btn.addEventListener('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);btn.textContent='Copied';setTimeout(()=>btn.textContent='Copy',1200)}catch{}});pre.appendChild(btn)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
|
||||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
|
||||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||getActiveSune().name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
|
||||||
function msgRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant';const meta=typeof m==='string'?{}:m||{};const row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');if(role==='user'){avatar.className='bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='🧑'}else{if(meta&&meta.avatar){avatar.className='msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden';const img=document.createElement('img');img.src=meta.avatar;img.className='h-full w-full object-cover';avatar.appendChild(img)}else{avatar.className='bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='✺'}}const name=document.createElement('div');name.className='text-xs font-medium text-gray-500';name.textContent=role==='user'?'You':getSuneLabel(meta);head.appendChild(avatar);head.appendChild(name);const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full';row.appendChild(head);row.appendChild(bubble);el.messages.appendChild(row);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));return bubble}
|
|
||||||
function renderMarkdown(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
|
||||||
function addMessage(m,track=true){const bubble=msgRow(m);renderMarkdown(bubble,m.content);if(track)state.messages.push(m);return bubble}
|
|
||||||
const addSuneBubbleStreaming=meta=>msgRow(Object.assign({role:'assistant'},meta))
|
|
||||||
const clearChat=()=>{state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
|
|
||||||
const payloadWithSampling=b=>Object.assign({},b,{temperature:store.temperature,top_p:store.top_p,top_k:store.top_k,frequency_penalty:store.frequency_penalty,presence_penalty:store.presence_penalty,repetition_penalty:store.repetition_penalty,min_p:store.min_p,top_a:store.top_a})
|
|
||||||
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.()}}
|
|
||||||
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
|
||||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.contentParts||m.content})));
|
|
||||||
let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort; if(re&&re!=='default')body.reasoning={effort:re};const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
|
||||||
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your OpenRouter API key.'}
|
|
||||||
const idb={db:null,open(){return new Promise((res,rej)=>{const r=indexedDB.open('chat_history_v1',1);r.onupgradeneeded=()=>{r.result.createObjectStore('threads',{keyPath:'id'})};r.onsuccess=()=>{this.db=r.result;res()};r.onerror=()=>rej(r.error)})},all(){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').getAll();tx.onsuccess=()=>res(tx.result||[]);tx.onerror=()=>rej(tx.error)})},get(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').get(id);tx.onsuccess=()=>res(tx.result||null);tx.onerror=()=>rej(tx.error)})},put(v){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').put(v);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})},del(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').delete(id);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})}}
|
|
||||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
|
||||||
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId){const existing=await idb.get(state.currentThreadId);if(!existing)needNew=true}if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await idb.put(th);await renderHistory()}
|
|
||||||
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId)||await idb.get(state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(th.messages.find(m=>m.role==='user')?.content||th.title);await idb.put(th);await renderHistory()}
|
|
||||||
const historyRow=t=>`<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class=\"h-4 w-4\"></i></button></div>`
|
|
||||||
async function renderHistory(){threads=(await idb.all()).sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=threads.map(historyRow).join('');icons()}
|
|
||||||
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
|
|
||||||
function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=(r.bottom+4)+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.historyMenu.classList.remove('hidden');icons()}
|
|
||||||
let menuSuneId=null;const hideSuneMenu=()=>{el.suneMenu.classList.add('hidden');menuSuneId=null}
|
|
||||||
function showSuneMenu(btn,id){menuSuneId=id;const r=btn.getBoundingClientRect();el.suneMenu.style.top=(r.bottom+4)+'px';el.suneMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.suneMenu.classList.remove('hidden');icons()}
|
|
||||||
el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),th=threads.find(t=>t.id===id)||await idb.get(id);if(!th)return;state.currentThreadId=id;clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);renderMarkdown(b,m.content)}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
|
|
||||||
el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId)||await idb.get(menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned;await idb.put(th)}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();await idb.put(th)}}else if(act==='delete'){if(confirm('Delete this chat?')){await idb.del(th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=Array.isArray(th.messages)?th.messages:[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(m.content||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideHistoryMenu();renderHistory()})
|
|
||||||
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}})
|
|
||||||
el.suneMenu.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=sunes.find(x=>x.id===menuSuneId);if(!s)return;if(act==='pin')s.pinned=!s.pinned;else if(act==='rename'){const nv=prompt('Rename sune to:',s.name);if(nv!=null)s.name=nv.trim()||s.name}else if(act==='pfp'){const url=prompt('Image URL:',s.avatar||'');if(url!==null)s.avatar=url.trim()}s.updatedAt=Date.now();su.save(sunes);hideSuneMenu();renderSidebar();reflectActiveSune()})
|
|
||||||
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
|
|
||||||
async function fileToPart(file){const name=file.name||'file',type=(file.type||'').toLowerCase();const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)});if(type.startsWith('image/')||/\.(png|jpe?g|webp|gif)$/i.test(name)){const url=await asDataURL(file);return {label:name,part:{type:'image_url',image_url:{url:url}}}}
|
|
||||||
if(type==='application/pdf'||/\.pdf$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';return {label:name,part:{type:'file',file:{filename:name,file_data:base64}}}}
|
|
||||||
if(type.startsWith('audio/')||/\.(wav|mp3)$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';let fmt='wav';if(/mp3/.test(type)||/\.mp3$/i.test(name))fmt='mp3';else if(/wav/.test(type)||/\.wav$/i.test(name))fmt='wav';return {label:name,part:{type:'input_audio',input_audio:{data:base64,format:fmt}}}}
|
|
||||||
return null}
|
|
||||||
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
|
|
||||||
el.fileInput.addEventListener('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const part=await fileToPart(f).catch(()=>null);if(part)state.attachments.push(part)}updateAttachBadge()})
|
|
||||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const names=state.attachments.map(a=>a.label).join(', ');const display=text+(names?`\n\n**Attachments:** ${esc(names)}`:'');const contentParts=[];if(text)contentParts.push({type:'text',text});state.attachments.forEach(a=>contentParts.push(a.part));addMessage({role:'user',content:display,contentParts});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}});state.attachments=[];updateAttachBadge()})
|
|
||||||
function openSettings(){const a=getActiveSune(),s=a.settings;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showModelTab();el.settingsModal.classList.remove('hidden')}
|
|
||||||
const closeSettings=()=>{el.settingsModal.classList.add('hidden')}
|
|
||||||
function showModelTab(){el.tabModel.classList.add('border-black');el.tabPrompt.classList.remove('border-black');el.panelModel.classList.remove('hidden');el.panelPrompt.classList.add('hidden')}
|
|
||||||
function showPromptTab(){el.tabPrompt.classList.add('border-black');el.tabModel.classList.remove('border-black');el.panelPrompt.classList.remove('hidden');el.panelModel.classList.add('hidden')}
|
|
||||||
el.settingsBtnTop.addEventListener('click',openSettings)
|
|
||||||
el.closeSettings.addEventListener('click',closeSettings)
|
|
||||||
el.cancelSettings.addEventListener('click',closeSettings)
|
|
||||||
el.settingsModal.addEventListener('click',e=>{if(e.target===el.settingsModal||e.target.classList.contains('bg-black/30'))closeSettings()})
|
|
||||||
el.tabModel.addEventListener('click',showModelTab)
|
|
||||||
el.tabPrompt.addEventListener('click',showPromptTab)
|
|
||||||
el.settingsForm.addEventListener('submit',e=>{e.preventDefault();const a=getActiveSune(),s=a.settings;s.model=(el.set_model.value||DEFAULT_MODEL).trim();s.temperature=clamp(num(el.set_temperature.value,1.0),0,2);s.top_p=clamp(num(el.set_top_p.value,1.0),0,1);s.top_k=Math.max(0,int(el.set_top_k.value,0));s.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);s.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);s.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);s.min_p=clamp(num(el.set_min_p.value,0.0),0,1);s.top_a=clamp(num(el.set_top_a.value,0.0),0,1);s.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();a.updatedAt=Date.now();su.save(sunes);closeSettings();reflectActiveSune()})
|
|
||||||
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=su.getActiveId(),active=getActiveSune(),name=active?.name||'this sune';if(!confirm(`Delete "${name}"?`))return;sunes=sunes.filter(a=>a.id!==activeId);su.save(sunes);if(sunes.length===0){const def=createDefaultSune();sunes=[def];su.save(sunes);su.setActiveId(def.id)}else{su.setActiveId(sunes[0].id)}renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
|
|
||||||
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const id=gid();sunes.unshift({id,name:name.trim(),pinned:false,avatar:'',updatedAt:Date.now(),settings:Object.assign({},defaultSettings)});su.save(sunes);su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')})
|
|
||||||
el.apiKeyOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');const cur=store.apiKey?'********':'';const input=prompt('Enter OpenRouter API key (stored locally):',cur);if(input!==null){store.apiKey=input==='********'?store.apiKey:input.trim();alert(store.apiKey?'API key saved locally.':'API key cleared.')}})
|
|
||||||
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
|
||||||
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
|
||||||
let importMode=null
|
|
||||||
el.sunesExportOption.addEventListener('click',()=>{dl(`sunes-${ts()}.json`,{version:1,sunes,activeId:su.getActiveId()});el.userMenu.classList.add('hidden')})
|
|
||||||
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
|
||||||
el.threadsExportOption.addEventListener('click',async()=>{const all=(await idb.all()).map(({createdAt,...t})=>t);dl(`threads-${ts()}.json`,{version:1,threads:all});el.userMenu.classList.add('hidden')})
|
|
||||||
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
|
||||||
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});su.save(sunes);if(data.activeId&&sunes.some(x=>x.id===data.activeId))su.setActiveId(data.activeId);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]});const best={};arr.forEach(t=>{const n=norm(t),k=n.id,prev=best[k];best[k]=!prev||(+n.updatedAt>+prev.updatedAt)?n:prev});let kept=0,skipped=0;for(const th of Object.values(best)){const ex=await idb.get(th.id);if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}await idb.put(th);kept++}await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
|
||||||
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
|
|
||||||
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:true}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
|
|
||||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
|
||||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
|
||||||
init()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didn’t register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const c=e=>t(e,s),d={module:{uri:s},exports:o,require:c};i[s]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"90533292e9888fcc85da6cc2f913f00d"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
|
||||||
205
index.html
@@ -1,198 +1,23 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
<load src="/src/parts/head.html" />
|
||||||
<title>Sune</title>
|
|
||||||
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif">
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css"/>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"/>
|
|
||||||
<style>
|
|
||||||
:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
|
|
||||||
.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
|
|
||||||
.msg-bubble{overflow-x:auto}
|
|
||||||
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:#0f172a;color:#fff;border-radius:.5rem;padding:.25rem .5rem;font-size:12px;opacity:.85}
|
|
||||||
.copy-btn:hover{opacity:1}
|
|
||||||
.msg-avatar{font-size:16px}
|
|
||||||
.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
|
|
||||||
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:.875rem;display:flex;align-items:center;gap:.5rem}
|
|
||||||
.menu-item:hover{background:#f9fafb}
|
|
||||||
</style>
|
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(!document.getElementById('historyMenu').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideHistoryMenu(); if(!document.getElementById('suneMenu').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSuneMenu(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
<body class="bg-white text-gray-900 selection:bg-black/10" x-data @click.window="if($event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains($event.target)&&!$event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains($event.target)&&!$event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains($event.target)&&!document.getElementById('userMenuBtn').contains($event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
||||||
<div class="flex flex-col h-dvh max-h-dvh">
|
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
|
||||||
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
<load src="/src/parts/topbar.html" />
|
||||||
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
<load src="/src/parts/chat.html" />
|
||||||
<button id="sidebarBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" hx-on="click:document.getElementById('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
<load src="/src/parts/footer.html" />
|
||||||
<button id="settingsBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
|
|
||||||
<div class="justify-self-end"><button id="historyBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" hx-on="click:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" hx-on="click: if(event.target.closest('.msg-avatar')){document.getElementById('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('hidden')}"></div><div class="h-24"></div></main>
|
|
||||||
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
|
||||||
<div class="mx-auto w-full max-w-none px-0">
|
|
||||||
<form id="composer" class="group relative flex items-start gap-2 px-3">
|
|
||||||
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
|
|
||||||
<div class="flex flex-col gap-2 self-stretch justify-center">
|
|
||||||
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
|
|
||||||
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
|
|
||||||
</div>
|
|
||||||
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<div id="sidebarOverlay" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:document.getElementById('sidebar').classList.add('-translate-x-full');this.classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full');document.getElementById('historyOverlay').classList.add('hidden');hideHistoryMenu();hideSuneMenu()"></div>
|
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
|
||||||
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
|
||||||
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
|
|
||||||
<div class="p-3 border-t relative">
|
|
||||||
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" hx-on="click:event.stopPropagation();document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
|
|
||||||
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
|
|
||||||
<button id="apiKeyOption" class="menu-item">Enter OpenRouter API key</button>
|
|
||||||
<button id="sunesImportOption" class="menu-item">Import sunes (.json)</button>
|
|
||||||
<button id="sunesExportOption" class="menu-item">Export sunes (.json)</button>
|
|
||||||
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
|
|
||||||
<button id="threadsExportOption" class="menu-item">Export threads (.json)</button>
|
|
||||||
</div>
|
|
||||||
<input id="importInput" type="file" accept="application/json,.json" class="hidden"/>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div id="historyOverlay" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:this.classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full')"></div>
|
|
||||||
<aside id="historyPanel" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
|
||||||
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>Threads</span><button id="closeHistory" class="p-1 rounded hover:bg-gray-100" aria-label="Close" hx-on="click:document.getElementById('historyOverlay').classList.add('hidden');document.getElementById('historyPanel').classList.add('translate-x-full')"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
|
||||||
<div id="historyList" class="flex-1 overflow-y-auto divide-y"></div>
|
|
||||||
</aside>
|
|
||||||
<div id="historyMenu" class="menu-card hidden">
|
|
||||||
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
|
||||||
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
|
||||||
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
|
|
||||||
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
|
|
||||||
</div>
|
|
||||||
<div id="suneMenu" class="menu-card hidden">
|
|
||||||
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
|
||||||
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
|
||||||
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
|
|
||||||
</div>
|
|
||||||
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
|
||||||
<div class="absolute inset-0 bg-black/30"></div>
|
|
||||||
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
|
|
||||||
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Sune Settings</span><button id="closeSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
|
||||||
<form id="settingsForm" class="text-sm">
|
|
||||||
<div class="border-b flex text-sm font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button></div>
|
|
||||||
<div id="panelModel" class="p-4 space-y-4">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="openai/gpt-4o"/></div>
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Variety. Lower = predictable.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Nucleus sampling.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top K</label><input id="set_top_k" type="number" min="0" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0"/><p class="mt-1 text-xs text-gray-500">Token shortlist size.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Frequency Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_frequency_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage repeats by count.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Presence Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_presence_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage seen tokens.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Reduce verbatim echoes.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Minimum token prob vs best.</p></div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Adaptive nucleus filter.</p></div>
|
|
||||||
</div>
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Default</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">Used only if supported by the model. (Default = Omitted)</p></div>
|
|
||||||
</div>
|
|
||||||
<div id="panelPrompt" class="p-4 space-y-4 hidden">
|
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black/20" placeholder="Enter a system prompt to guide the sune"></textarea><p class="mt-1 text-xs text-gray-500">Saved per sune.</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
|
||||||
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button>
|
|
||||||
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<load src="/src/parts/sidebars.html" />
|
||||||
|
<load src="/src/parts/modals.html" />
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
||||||
<script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
|
||||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','panelModel','panelPrompt','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput'].map(id=>[id,document.getElementById(id)]))
|
|
||||||
const icons=()=>window.lucide&&lucide.createIcons()
|
|
||||||
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c]))
|
|
||||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}}
|
|
||||||
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
|
||||||
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,reasoning_effort:'default',system_prompt:''}
|
|
||||||
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
|
|
||||||
let sunes=(su.load()||[]).map(makeSune)
|
|
||||||
if(!sunes.length){const def=makeSune({name:'Default'});sunes=[def];su.save(sunes);su.setActiveId(def.id)}
|
|
||||||
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>makeSune({name:'Default'})
|
|
||||||
const store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;const a=getActiveSune();if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='apiKey'){globalStore.apiKey=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}})
|
|
||||||
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[]}
|
|
||||||
const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm}
|
|
||||||
const reflectActiveSune=()=>{const a=getActiveSune();el.settingsBtnTop.title=`Settings — ${a.name}`;el.settingsBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';icons()}
|
|
||||||
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===su.getActiveId()?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-6 w-6 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
|
|
||||||
const renderSidebar=()=>{const list=[...sunes].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
|
|
||||||
function enhanceCodeBlocks(root,doHL=true){root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn';btn.textContent='Copy';btn.addEventListener('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);btn.textContent='Copied';setTimeout(()=>btn.textContent='Copy',1200)}catch{}});pre.appendChild(btn)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
|
||||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
|
||||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||getActiveSune().name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
|
||||||
function msgRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant';const meta=typeof m==='string'?{}:m||{};const row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');if(role==='user'){avatar.className='bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='🧑'}else{if(meta&&meta.avatar){avatar.className='msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden';const img=document.createElement('img');img.src=meta.avatar;img.className='h-full w-full object-cover';avatar.appendChild(img)}else{avatar.className='bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='✺'}}const name=document.createElement('div');name.className='text-xs font-medium text-gray-500';name.textContent=role==='user'?'You':getSuneLabel(meta);head.appendChild(avatar);head.appendChild(name);const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full';row.appendChild(head);row.appendChild(bubble);el.messages.appendChild(row);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));return bubble}
|
|
||||||
function renderMarkdown(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
|
||||||
function addMessage(m,track=true){const bubble=msgRow(m);renderMarkdown(bubble,m.content);if(track)state.messages.push(m);return bubble}
|
|
||||||
const addSuneBubbleStreaming=meta=>msgRow(Object.assign({role:'assistant'},meta))
|
|
||||||
const clearChat=()=>{state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
|
|
||||||
const payloadWithSampling=b=>Object.assign({},b,{temperature:store.temperature,top_p:store.top_p,top_k:store.top_k,frequency_penalty:store.frequency_penalty,presence_penalty:store.presence_penalty,repetition_penalty:store.repetition_penalty,min_p:store.min_p,top_a:store.top_a})
|
|
||||||
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.()}}
|
|
||||||
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
|
||||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.contentParts||m.content})));
|
|
||||||
let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort; if(re&&re!=='default')body.reasoning={effort:re};const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
|
||||||
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your OpenRouter API key.'}
|
|
||||||
const idb={db:null,open(){return new Promise((res,rej)=>{const r=indexedDB.open('chat_history_v1',1);r.onupgradeneeded=()=>{r.result.createObjectStore('threads',{keyPath:'id'})};r.onsuccess=()=>{this.db=r.result;res()};r.onerror=()=>rej(r.error)})},all(){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').getAll();tx.onsuccess=()=>res(tx.result||[]);tx.onerror=()=>rej(tx.error)})},get(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').get(id);tx.onsuccess=()=>res(tx.result||null);tx.onerror=()=>rej(tx.error)})},put(v){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').put(v);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})},del(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').delete(id);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})}}
|
|
||||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
|
||||||
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId){const existing=await idb.get(state.currentThreadId);if(!existing)needNew=true}if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await idb.put(th);await renderHistory()}
|
|
||||||
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId)||await idb.get(state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(th.messages.find(m=>m.role==='user')?.content||th.title);await idb.put(th);await renderHistory()}
|
|
||||||
const historyRow=t=>`<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class=\"h-4 w-4\"></i></button></div>`
|
|
||||||
async function renderHistory(){threads=(await idb.all()).sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=threads.map(historyRow).join('');icons()}
|
|
||||||
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
|
|
||||||
function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=(r.bottom+4)+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.historyMenu.classList.remove('hidden');icons()}
|
|
||||||
let menuSuneId=null;const hideSuneMenu=()=>{el.suneMenu.classList.add('hidden');menuSuneId=null}
|
|
||||||
function showSuneMenu(btn,id){menuSuneId=id;const r=btn.getBoundingClientRect();el.suneMenu.style.top=(r.bottom+4)+'px';el.suneMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.suneMenu.classList.remove('hidden');icons()}
|
|
||||||
el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),th=threads.find(t=>t.id===id)||await idb.get(id);if(!th)return;state.currentThreadId=id;clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);renderMarkdown(b,m.content)}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
|
|
||||||
el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId)||await idb.get(menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned;await idb.put(th)}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();await idb.put(th)}}else if(act==='delete'){if(confirm('Delete this chat?')){await idb.del(th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=Array.isArray(th.messages)?th.messages:[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(m.content||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideHistoryMenu();renderHistory()})
|
|
||||||
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}})
|
|
||||||
el.suneMenu.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=sunes.find(x=>x.id===menuSuneId);if(!s)return;if(act==='pin')s.pinned=!s.pinned;else if(act==='rename'){const nv=prompt('Rename sune to:',s.name);if(nv!=null)s.name=nv.trim()||s.name}else if(act==='pfp'){const url=prompt('Image URL:',s.avatar||'');if(url!==null)s.avatar=url.trim()}s.updatedAt=Date.now();su.save(sunes);hideSuneMenu();renderSidebar();reflectActiveSune()})
|
|
||||||
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
|
|
||||||
async function fileToPart(file){const name=file.name||'file',type=(file.type||'').toLowerCase();const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)});if(type.startsWith('image/')||/\.(png|jpe?g|webp|gif)$/i.test(name)){const url=await asDataURL(file);return {label:name,part:{type:'image_url',image_url:{url:url}}}}
|
|
||||||
if(type==='application/pdf'||/\.pdf$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';return {label:name,part:{type:'file',file:{filename:name,file_data:base64}}}}
|
|
||||||
if(type.startsWith('audio/')||/\.(wav|mp3)$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';let fmt='wav';if(/mp3/.test(type)||/\.mp3$/i.test(name))fmt='mp3';else if(/wav/.test(type)||/\.wav$/i.test(name))fmt='wav';return {label:name,part:{type:'input_audio',input_audio:{data:base64,format:fmt}}}}
|
|
||||||
return null}
|
|
||||||
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
|
|
||||||
el.fileInput.addEventListener('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const part=await fileToPart(f).catch(()=>null);if(part)state.attachments.push(part)}updateAttachBadge()})
|
|
||||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const names=state.attachments.map(a=>a.label).join(', ');const display=text+(names?`\n\n**Attachments:** ${esc(names)}`:'');const contentParts=[];if(text)contentParts.push({type:'text',text});state.attachments.forEach(a=>contentParts.push(a.part));addMessage({role:'user',content:display,contentParts});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}});state.attachments=[];updateAttachBadge()})
|
|
||||||
function openSettings(){const a=getActiveSune(),s=a.settings;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showModelTab();el.settingsModal.classList.remove('hidden')}
|
|
||||||
const closeSettings=()=>{el.settingsModal.classList.add('hidden')}
|
|
||||||
function showModelTab(){el.tabModel.classList.add('border-black');el.tabPrompt.classList.remove('border-black');el.panelModel.classList.remove('hidden');el.panelPrompt.classList.add('hidden')}
|
|
||||||
function showPromptTab(){el.tabPrompt.classList.add('border-black');el.tabModel.classList.remove('border-black');el.panelPrompt.classList.remove('hidden');el.panelModel.classList.add('hidden')}
|
|
||||||
el.settingsBtnTop.addEventListener('click',openSettings)
|
|
||||||
el.closeSettings.addEventListener('click',closeSettings)
|
|
||||||
el.cancelSettings.addEventListener('click',closeSettings)
|
|
||||||
el.settingsModal.addEventListener('click',e=>{if(e.target===el.settingsModal||e.target.classList.contains('bg-black/30'))closeSettings()})
|
|
||||||
el.tabModel.addEventListener('click',showModelTab)
|
|
||||||
el.tabPrompt.addEventListener('click',showPromptTab)
|
|
||||||
el.settingsForm.addEventListener('submit',e=>{e.preventDefault();const a=getActiveSune(),s=a.settings;s.model=(el.set_model.value||DEFAULT_MODEL).trim();s.temperature=clamp(num(el.set_temperature.value,1.0),0,2);s.top_p=clamp(num(el.set_top_p.value,1.0),0,1);s.top_k=Math.max(0,int(el.set_top_k.value,0));s.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);s.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);s.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);s.min_p=clamp(num(el.set_min_p.value,0.0),0,1);s.top_a=clamp(num(el.set_top_a.value,0.0),0,1);s.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();a.updatedAt=Date.now();su.save(sunes);closeSettings();reflectActiveSune()})
|
|
||||||
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=su.getActiveId(),active=getActiveSune(),name=active?.name||'this sune';if(!confirm(`Delete "${name}"?`))return;sunes=sunes.filter(a=>a.id!==activeId);su.save(sunes);if(sunes.length===0){const def=createDefaultSune();sunes=[def];su.save(sunes);su.setActiveId(def.id)}else{su.setActiveId(sunes[0].id)}renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
|
|
||||||
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const id=gid();sunes.unshift({id,name:name.trim(),pinned:false,avatar:'',updatedAt:Date.now(),settings:Object.assign({},defaultSettings)});su.save(sunes);su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')})
|
|
||||||
el.apiKeyOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');const cur=store.apiKey?'********':'';const input=prompt('Enter OpenRouter API key (stored locally):',cur);if(input!==null){store.apiKey=input==='********'?store.apiKey:input.trim();alert(store.apiKey?'API key saved locally.':'API key cleared.')}})
|
|
||||||
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
|
||||||
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
|
||||||
let importMode=null
|
|
||||||
el.sunesExportOption.addEventListener('click',()=>{dl(`sunes-${ts()}.json`,{version:1,sunes,activeId:su.getActiveId()});el.userMenu.classList.add('hidden')})
|
|
||||||
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
|
||||||
el.threadsExportOption.addEventListener('click',async()=>{const all=(await idb.all()).map(({createdAt,...t})=>t);dl(`threads-${ts()}.json`,{version:1,threads:all});el.userMenu.classList.add('hidden')})
|
|
||||||
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
|
||||||
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});su.save(sunes);if(data.activeId&&sunes.some(x=>x.id===data.activeId))su.setActiveId(data.activeId);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]});const best={};arr.forEach(t=>{const n=norm(t),k=n.id,prev=best[k];best[k]=!prev||(+n.updatedAt>+prev.updatedAt)?n:prev});let kept=0,skipped=0;for(const th of Object.values(best)){const ex=await idb.get(th.id);if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}await idb.put(th);kept++}await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
|
||||||
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
|
|
||||||
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:true}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
|
|
||||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
|
||||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
|
||||||
init()
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^7.1.2",
|
"vite": "8.0.*",
|
||||||
"vite-plugin-pwa": "^1.0.2"
|
"vite-plugin-html-inject": "1.1.*",
|
||||||
|
"vite-plugin-pwa": "1.2.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
sune.planetrenox.com
|
|
||||||
BIN
public/appstore_content/latex.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 268 KiB |
BIN
public/appstore_content/screenshot4.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
public/appstore_content/screenshot5.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/appstore_content/screenshot6.jpg
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
public/appstore_content/screenshot_marketplace.jpg
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/appstore_content/screenshot_miku.png
Normal file
|
After Width: | Height: | Size: 805 KiB |
BIN
public/appstore_content/sync.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
public/✺.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
62
readme.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
[-green?style=for-the-badge&logo=android)](https://github.com/multipleof4/sune/releases/download/v0.23.0/sune-v0.23.0.apk)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> Each sune is like a module. You can have many. And share them.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
💠 New!
|
||||||
|
|
||||||
|
> You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> Image support.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> There is a marketplace.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> LaTeX support out of the box.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Sync Your Chats with GitHub
|
||||||
|
|
||||||
|
Never lose a conversation again. Sune can sync all your threads to a GitHub repo.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. **Create a GitHub repo** — can be private or public, whatever you prefer. Something like `.chats`.
|
||||||
|
|
||||||
|
2. **Generate a Personal Access Token (PAT)**
|
||||||
|
- Go to [github.com/settings/tokens](https://github.com/settings/tokens) → **Classic Token** → **Generate new token**
|
||||||
|
- Give it **Read and write** access to **Contents** on your repo
|
||||||
|
- Copy the token
|
||||||
|
|
||||||
|
3. **Add your token in Sune**
|
||||||
|
- Open the left sidebar → **Account & Backup** → **Settings**
|
||||||
|
- Go to the **API** tab
|
||||||
|
- Paste your token into the **Github Token** field
|
||||||
|
- Hit **Save**
|
||||||
|
|
||||||
|
4. **Point Sune to your repo**
|
||||||
|
- Open the right sidebar (Threads panel)
|
||||||
|
- In the repo input at the top, enter: `gh://your-username/.chats`
|
||||||
|
- Press Enter
|
||||||
|
|
||||||
|
5. **Sync**
|
||||||
|
- Hit the **Sync** button after starting a new chat in there.
|
||||||
|
- **OK** = Push your local threads up to GitHub
|
||||||
|
- **Cancel** = Pull threads down from GitHub
|
||||||
|
|
||||||
|
That's it. Your threads are now backed up as JSON files in your repo. You can sync across devices, never lose a chat, and even browse your conversations directly on GitHub.
|
||||||
157
src/main.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import {streamChat,HTTP_BASE} from './streaming.js';
|
||||||
|
import {SUNE_LOGO_SVG} from './sune-logo.js';
|
||||||
|
import {STICKY_SUNES} from './sticky-sunes.js';
|
||||||
|
import {generateTitleWithAI} from './title-generator.js';
|
||||||
|
import mathjax3 from 'https://esm.sh/markdown-it-mathjax3';
|
||||||
|
(()=>{let k,v=visualViewport;const f=()=>{removeEventListener('popstate',f),document.activeElement?.blur()};v.onresize=()=>{let o=v.height<innerHeight;o!=k&&((k=o)?(history.pushState({k:1},''),addEventListener('popstate',f)):(removeEventListener('popstate',f),history.state?.k&&history.back()))}})()
|
||||||
|
const DEFAULT_MODEL='anthropic/claude-opus-4.6'
|
||||||
|
const el=window.el=Object.fromEntries(['topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_verbosity','set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts','set_json_output','set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer','set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','importInput','sidebarBtnRight','sidebarRight','sidebarOverlayRight','threadList','closeThreads','threadPopover','sunePopover','footer','attachBtn','attachBadge','fileInput','htmlEditor','extensionHtmlEditor','jsonSchemaEditor','htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_api_key_g','set_api_key_claude','set_api_key_cf','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','gcpSAInput','gcpSAUploadBtn','importAccountSettings','exportAccountSettings','importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name','userAvatarPreview','setUserAvatarBtn','userAvatarInput','threadRepoInput','threadBackBtn','threadFolderBtn','threadSyncBtn'].map(id=>[id,$('#'+id)[0]]))
|
||||||
|
const icons=()=>window.lucide&&lucide.createIcons()
|
||||||
|
const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1)
|
||||||
|
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c])),positionPopover=(a,p)=>{const r=a.getBoundingClientRect();p.style.top=`${r.bottom+p.offsetHeight+4>window.innerHeight?r.top-p.offsetHeight-4:r.bottom+4}px`;p.style.left=`${Math.max(8,Math.min(r.right-p.offsetWidth,window.innerWidth-p.offsetWidth-8))}px`}
|
||||||
|
const sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6)
|
||||||
|
const fmtSize=b=>{const u=['B','KB','MB','GB','TB'];let i=0,x=b;while(x>=1024&&i<u.length-1){x/=1024;i++}return (x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i]}
|
||||||
|
const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)})
|
||||||
|
const imgToWebp=(f,D=128,q=80)=>new Promise((r,j)=>{if(!f)return j();const i=new Image;i.onload=()=>{const c=document.createElement('canvas'),x=c.getContext('2d');let w=i.width,h=i.height;if(D>0&&Math.max(w,h)>D)w>h?(h=D*h/w,w=D):(w=D*w/h,h=D);c.width=w;c.height=h;x.drawImage(i,0,0,w,h);r(c.toDataURL('image/webp',clamp(q,0,100)/100));URL.revokeObjectURL(i.src)};i.onerror=j;i.src=URL.createObjectURL(f)});
|
||||||
|
const b64=x=>x.split(',')[1]||''
|
||||||
|
const utob=s=>btoa(unescape(encodeURIComponent(s))),btou=s=>decodeURIComponent(escape(atob(s.replace(/\s/g,''))))
|
||||||
|
|
||||||
|
const ghApi=async(path,method='GET',body=null)=>{const t=USER.githubToken;if(!t)throw new Error('No GH token');const r=await fetch(`https://api.github.com/repos/${path}`,{method,headers:{'Authorization':`token ${t}`,'Accept':'application/vnd.github.v3+json','Content-Type':'application/json'},body:body?JSON.stringify(body):null});if(!r.ok&&r.status!==404)throw new Error(`GH API ${r.status}`);return r.status===404?null:r.json()};
|
||||||
|
const parseGhUrl=u=>{const p=u.substring(5).split('/'),owner=p[0],repoPart=p[1]||'',branch=repoPart.includes('@')?repoPart.split('@')[1]:'main',repo=repoPart.split('@')[0],path=p.slice(2).join('/').replace(/\/$/,'');return{owner,repo,branch,path,apiPath:`${owner}/${repo}/contents${path?'/'+path:''}`}};
|
||||||
|
|
||||||
|
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
||||||
|
const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,json_output:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false,json_schema:''}
|
||||||
|
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{}),storage:p.storage||{}})
|
||||||
|
let sunes=(su.load()||[]).map(makeSune)
|
||||||
|
const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return true}})
|
||||||
|
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
|
||||||
|
const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
|
||||||
|
const getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
|
||||||
|
const resolveSuneSrc=src=>{if(!src)return null;if(src.startsWith('gh://')){const path=src.substring(5),parts=path.split('/');if(parts.length<3)return null;const[owner,repo,...filePathParts]=parts;return`https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join('/')}`}return src}
|
||||||
|
const processSuneIncludes=async(html,depth=0)=>{if(depth>5)return'<!-- Sune include depth limit reached -->';if(!html)return'';const c=document.createElement('div');c.innerHTML=html;for(const n of[...c.querySelectorAll('sune')]){if(n.hasAttribute('src')){if(n.hasAttribute('private')&&depth>0){n.remove();continue}const s=n.getAttribute('src'),u=resolveSuneSrc(s);if(!u){n.replaceWith(document.createComment(` Invalid src: ${esc(s)} `));continue}try{const r=await fetch(u);if(!r.ok)throw new Error(`HTTP ${r.status}`);const d=await r.json(),o=Array.isArray(d)?d[0]:d,h=[o?.settings?.extension_html||'',o?.settings?.html||''].join('\n');n.replaceWith(document.createRange().createContextualFragment(await processSuneIncludes(h,depth+1)))}catch(e){n.replaceWith(document.createComment(` Fetch failed: ${esc(u)} `))}}else{n.replaceWith(document.createRange().createContextualFragment(n.innerHTML))}}return c.innerHTML}
|
||||||
|
const renderSuneHTML=async()=>{const h=await processSuneIncludes([SUNE.extension_html,SUNE.html].map(x=>(x||'').trim()).join('\n')),c=el.suneHtml;c.innerHTML='';const t=h.trim();c.classList.toggle('hidden',!t);t&&(c.appendChild(document.createRange().createContextualFragment(h)),window.Alpine?.initTree(c))}
|
||||||
|
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
|
||||||
|
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
|
||||||
|
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
|
||||||
|
function enhanceCodeBlocks(root,doHL=true){$(root).find('pre>code').each((i,code)=>{if(code.textContent.length>200000)return;const $pre=$(code).parent().addClass('relative rounded-xl border border-gray-200');if(!$pre.find('.code-actions').length){const len=code.textContent.length,countText=len>=1e3?(len/1e3).toFixed(1)+'K':len;const $btn=$('<button class="bg-slate-900 text-white rounded-lg py-1 px-2 text-xs opacity-85">Copy</button>').on('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);$btn.text('Copied');setTimeout(()=>$btn.text('Copy'),1200)}catch{}});const $container=$('<div class="code-actions absolute top-2 right-2 flex items-center gap-2"></div>');$container.append($(`<span class="text-xs text-gray-500">${countText} chars</span>`),$btn);$pre.append($container)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||||
|
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true}).use(mathjax3)
|
||||||
|
const getSuneLabel=m=>{const name=(m&&m.sune_name)||SUNE.name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
||||||
|
function _createMessageRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant',meta=typeof m==='string'?{}:m||{},isUser=role==='user',$row=$('<div class="flex flex-col gap-2"></div>'),$head=$('<div class="flex items-center gap-2 px-4"></div>'),$avatar=$('<div></div>');const uAva=isUser?USER.avatar:meta.avatar;uAva?$avatar.attr('class','msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden').html(`<img src="${esc(uAva)}" class="h-full w-full object-cover">`):$avatar.attr('class',`${isUser?'bg-gray-900 text-white':'bg-gray-200 text-gray-900'} msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center`).text(isUser?'👤':'✺');const $name=$('<div class="text-xs font-medium text-gray-500"></div>').text(isUser?USER.name:getSuneLabel(meta));const $deleteBtn=$('<button class="p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500" title="Delete message"><i data-lucide="x" class="h-4 w-4"></i></button>').on('click',async e=>{e.stopPropagation();state.messages=state.messages.filter(msg=>msg.id!==m.id);$row.remove();await THREAD.persist()});const $copyBtn=$('<button class="ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Copy message"><i data-lucide="copy" class="h-4 w-4"></i></button>').on('click',async function(e){e.stopPropagation();try{await navigator.clipboard.writeText(partsToText(m));$(this).html('<i data-lucide="check" class="h-4 w-4 text-green-500"></i>');icons();setTimeout(()=>{$(this).html('<i data-lucide="copy" class="h-4 w-4"></i>');icons()},1200)}catch{}});$head.append($avatar,$name,$copyBtn,$deleteBtn);const $bubble=$(`<div class="${(isUser?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full'}"></div>`);$row.append($head,$bubble);return $row}
|
||||||
|
function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);queueMicrotask(()=>{el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'});icons()});return $row.find('.msg-bubble')[0]}
|
||||||
|
const renderMarkdown=window.renderMarkdown=function(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
||||||
|
function partsToText(m){if(!m)return'';const c=m.content,i=m.images;let t=Array.isArray(c)?c.map(p=>p?.type==='text'?p.text:(p?.type==='image_url'?``:(p?.type==='file'?`[${p.file?.filename||'file'}]`:(p?.type==='input_audio'?`(audio:${p.input_audio?.format||''})`:'')))).join('\n'):String(c||'');if(Array.isArray(i))t+=i.map(x=>`\n\n`).join('');return t}
|
||||||
|
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
|
||||||
|
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
|
||||||
|
const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
|
||||||
|
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
|
||||||
|
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}}
|
||||||
|
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
||||||
|
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your API key.'}
|
||||||
|
const titleFrom=t=>{if(!t)return'Untitled';const s=typeof t==='string'?t:(Array.isArray(t)?partsToText({content:t}):'Untitled');return s.replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'}
|
||||||
|
const serializeThreadName=t=>{const s=(t.title||'Untitled').replace(/[^a-zA-Z0-9]/g,'_').slice(0,150);return `${t.pinned?'1':'0'}-${t.updatedAt||Date.now()}-${t.id}-${s}.json`}
|
||||||
|
const deserializeThreadName=n=>{const p=n.replace('.json','').split('-');if(p.length<4)return null;return {pinned:p[0]==='1',updatedAt:parseInt(p[1]),id:p[2],title:p.slice(3).join('-').replace(/_/g,' '),status:'synced',type:'thread'}}
|
||||||
|
const TKEY='threads_v1',THREAD=window.THREAD={list:[],load:async function(){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){this.list=await localforage.getItem('rem_index_'+u.substring(5)).then(v=>Array.isArray(v)?v:[])||[]}else{this.list=await localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[])||[]}},save:async function(){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){await localforage.setItem('rem_index_'+u.substring(5),this.list.map(t=>{const n={...t};delete n.messages;return n}))}else{await localforage.setItem(TKEY,this.list.map(t=>{const n={...t};delete n.messages;return n}))}},get:function(id){return this.list.find(t=>t.id===id)},get active(){return this.get(state.currentThreadId)},persist:async function(full=true){const id=state.currentThreadId;if(!id)return;const meta=this.get(id);if(!meta)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';await localforage.setItem(prefix+id,[...state.messages]);if(full){meta.updatedAt=Date.now();if(u.startsWith('gh://')&&meta.status!=='new')meta.status='modified';await this.save();await renderThreads()}},setTitle:async function(id,title){const th=this.get(id);if(!th||!title)return;th.title=titleFrom(title);th.updatedAt=Date.now();const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified';await this.save();await renderThreads()},getLastAssistantMessageId:()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(!h)continue;if(!/^\s*You\b/.test(h.textContent||''))return b.dataset.mid||null}return null}}
|
||||||
|
const cacheStore=localforage.createInstance({name:'threads_cache',storeName:'streams_status'});
|
||||||
|
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!THREAD.get(state.currentThreadId))needNew=true;if(!needNew)return;const id=gid(),now=Date.now(),u=el.threadRepoInput.value.trim(),th={id,title:'',pinned:false,updatedAt:now,type:'thread'};if(u.startsWith('gh://'))th.status='new';state.currentThreadId=id;THREAD.list.unshift(th);await THREAD.save();const prefix=u.startsWith('gh://')?'rem_t_':'t_';await localforage.setItem(prefix+id,[]);await renderThreads()}
|
||||||
|
const threadRow=t=>{const icon=t.type==='folder'?'folder':(t.type==='file'?'file-text':'');return `<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" data-type=\"${t.type||'thread'}\" class=\"flex-1 text-left truncate flex items-center gap-2\">${icon?`<i data-lucide="${icon}" class="h-4 w-4"></i>`:''}${t.pinned?'📌 ':''}${esc(t.title||'Untitled')}${t.status==='modified'?'*':(t.status==='new'?'+':'')}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class="h-4 w-4"></i></button></div>`}
|
||||||
|
let sortedThreads=[],isAddingThreads=false;const THREAD_PAGE_SIZE=50;
|
||||||
|
async function renderThreads(){
|
||||||
|
sortedThreads=[...THREAD.list].filter(t=>t.status!=='deleted').sort((a,b)=>{
|
||||||
|
if(a.type==='file'&&b.type!=='file')return -1;
|
||||||
|
if(a.type!=='file'&&b.type==='file')return 1;
|
||||||
|
return (b.pinned-a.pinned)||(b.updatedAt-a.updatedAt);
|
||||||
|
});
|
||||||
|
el.threadList.innerHTML=sortedThreads.slice(0,THREAD_PAGE_SIZE).map(threadRow).join('');
|
||||||
|
el.threadList.scrollTop=0;
|
||||||
|
isAddingThreads=false;
|
||||||
|
icons()
|
||||||
|
}
|
||||||
|
let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.add('hidden');menuThreadId=null}
|
||||||
|
function showThreadPopover(btn,id){menuThreadId=id;el.threadPopover.classList.remove('hidden');positionPopover(btn,el.threadPopover);icons()}
|
||||||
|
let menuSuneId=null;const hideSunePopover=()=>{el.sunePopover.classList.add('hidden');menuSuneId=null}
|
||||||
|
function showSunePopover(btn,id){menuSuneId=id;el.sunePopover.classList.remove('hidden');positionPopover(btn,el.sunePopover);icons()}
|
||||||
|
$(el.threadList).on('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),type=openBtn.getAttribute('data-type');if(type==='file'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);window.open(`https://github.com/${info.owner}/${info.repo}/blob/${info.branch}/${id}`,'_blank')}return}if(type==='folder'){const u=el.threadRepoInput.value.trim();el.threadRepoInput.value=u+(u.endsWith('/')?'':'/')+id;el.threadRepoInput.dispatchEvent(new Event('change'));return}if(id!==state.currentThreadId&&state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null}const th=THREAD.get(id);if(!th)return;if(id===state.currentThreadId){el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}state.currentThreadId=id;clearChat();const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';let msgs=await localforage.getItem(prefix+id);if(!msgs&&u.startsWith('gh://')){try{const info=parseGhUrl(u),fileName=serializeThreadName(th),res=await ghApi(`${info.apiPath}/${fileName}?ref=${info.branch}`);if(res&&res.content){msgs=JSON.parse(btou(res.content));await localforage.setItem(prefix+id,msgs);th.status='synced';await THREAD.save()}}catch(e){console.error('Remote fetch failed',e)}}state.messages=Array.isArray(msgs)?[...msgs]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m))}await renderSuneHTML();syncWhileBusy();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}if(menuBtn){e.stopPropagation();showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
|
||||||
|
$(el.threadList).on('scroll',()=>{
|
||||||
|
if(isAddingThreads||el.threadList.scrollTop+el.threadList.clientHeight<el.threadList.scrollHeight-200)return;
|
||||||
|
const c=el.threadList.children.length;
|
||||||
|
if(c>=sortedThreads.length)return;
|
||||||
|
isAddingThreads=true;
|
||||||
|
const b=sortedThreads.slice(c,c+THREAD_PAGE_SIZE);
|
||||||
|
if(b.length){
|
||||||
|
el.threadList.insertAdjacentHTML('beforeend',b.map(threadRow).join(''));
|
||||||
|
icons();
|
||||||
|
}
|
||||||
|
isAddingThreads=false;
|
||||||
|
});
|
||||||
|
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(menuThreadId);if(!th)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);try{await navigator.clipboard.writeText(`${info.owner}/${info.repo}@${info.branch}/${th.id}`);alert('Path copied.')}catch{}}}hideThreadPopover();await THREAD.save();renderThreads()})
|
||||||
|
$(el.suneList).on('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}})
|
||||||
|
$(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();await reflectActiveSune()};if(act==='pin'){s.pinned=!s.pinned;await updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();await updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
|
||||||
|
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
|
||||||
|
async function toAttach(file){if(!file)return null;if(file instanceof File){const name=file.name||'file',mime=(file.type||'application/octet-stream').toLowerCase();if(/^image\//.test(mime)||/\.(png|jpe?g|webp|gif)$/i.test(name)){const data=mime==='image/webp'||/\.webp$/i.test(name)?await asDataURL(file):await imgToWebp(file,2048,94);return{type:'image_url',image_url:{url:data}}}if(mime==='application/pdf'||/\.pdf$/i.test(name)){const data=await asDataURL(file),bin=b64(data);return{type:'file',file:{filename:name.endsWith('.pdf')?name:name+'.pdf',file_data:bin}}}if(/^audio\//.test(mime)||/\.(wav|mp3)$/i.test(name)){const data=await asDataURL(file),bin=b64(data),fmt=/mp3/.test(mime)||/\.mp3$/i.test(name)?'mp3':'wav';return{type:'input_audio',input_audio:{data:bin,format:fmt}}}const data=await asDataURL(file),bin=b64(data);return{type:'file',file:{filename:name,file_data:bin}}}if(file&&file.name==null&&file.data){const name=file.name||'file',mime=(file.mime||'application/octet-stream').toLowerCase();if(/^image\//.test(mime)){const url=`data:${mime};base64,${file.data}`;return{type:'image_url',image_url:{url}}}if(mime==='application/pdf'){return{type:'file',file:{filename:name,file_data:file.data}}}if(/^audio\//.test(mime)){const fmt=/mp3/.test(mime)?'mp3':'wav';return{type:'input_audio',input_audio:{data:file.data,format:fmt}}}return{type:'file',file:{filename:name,file_data:file.data}}}return null}
|
||||||
|
$(el.attachBtn).on('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
|
||||||
|
$(el.fileInput).on('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
|
||||||
|
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId);state.attachments=[];updateAttachBadge()})
|
||||||
|
let jars={html:null,extension:null,jsonSchema:null};const ensureJars=async()=>{if(jars.html&&jars.extension&&jars.jsonSchema)return jars;const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default,hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value,hl_json=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'json'}).value;if(!jars.html)jars.html=CodeJar(el.htmlEditor,hl,{tab:' '});if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor,hl,{tab:' '});if(!jars.jsonSchema)jars.jsonSchema=CodeJar(el.jsonSchemaEditor,hl_json,{tab:' '});return jars}
|
||||||
|
let openedHTML=false
|
||||||
|
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;el.set_json_output.checked=!!s.json_output;el.set_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
|
||||||
|
const closeSettings=()=>{el.suneModal.classList.add('hidden')}
|
||||||
|
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
|
||||||
|
function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Prompt'){ensureJars().then(({jsonSchema})=>{const s=SUNE.settings;jsonSchema.updateCode(s.json_schema||'')})}else if(key==='Script'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}}
|
||||||
|
$(el.suneBtnTop).on('click',openSettings)
|
||||||
|
$(el.cancelSettings).on('click',closeSettings)
|
||||||
|
$(el.suneModal).on('click',e=>{if(e.target===el.suneModal||e.target.classList.contains('bg-black/30'))closeSettings()})
|
||||||
|
$(el.tabModel).on('click',()=>showTab('Model'))
|
||||||
|
$(el.tabPrompt).on('click',()=>showTab('Prompt'))
|
||||||
|
$(el.tabScript).on('click',()=>showTab('Script'))
|
||||||
|
$(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked))
|
||||||
|
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.json_output=el.set_json_output.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;SUNE.json_schema=el.jsonSchemaEditor.textContent;if(openedHTML){SUNE.html=el.htmlEditor.textContent;SUNE.extension_html=el.extensionHtmlEditor.textContent}closeSettings();await reflectActiveSune()})
|
||||||
|
$(el.deleteSuneBtn).on('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
|
||||||
|
$(el.newSuneBtn).on('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')})
|
||||||
|
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:name.endsWith('.sune')?'application/octet-stream':'application/json'}),url=URL.createObjectURL(blob),a=$('<a>').prop({href:url,download:name}).appendTo('body');a.get(0).click();a.remove();URL.revokeObjectURL(url)}
|
||||||
|
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
||||||
|
let importMode=null
|
||||||
|
$(el.sunesExportOption).on('click',()=>{dl(`sunes-${ts()}.sune`,{version:1,sunes:SUNE.list,activeId:SUNE.id});el.userMenu.classList.add('hidden')})
|
||||||
|
$(el.sunesImportOption).on('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
||||||
|
$(el.threadsImportOption).on('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
||||||
|
$(el.importInput).on('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});SUNE.save();if(data.activeId&&sunes.some(x=>x.id===data.activeId))SUNE.setActive(data.activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){if(!data||!data.id||!Array.isArray(data.messages))throw new Error('Invalid thread format');const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||t.messages),pinned:!!t.pinned,updatedAt:num(t.updatedAt,Date.now()),type:'thread',...(u.startsWith('gh://')?{status:'new'}:{})});const n=norm(data),msgs=data.messages,idx=THREAD.list.findIndex(x=>x.id===n.id);if(idx>-1){if(n.updatedAt>THREAD.list[idx].updatedAt){THREAD.list[idx]=n;await localforage.setItem(prefix+n.id,msgs)}}else{THREAD.list.unshift(n);await localforage.setItem(prefix+n.id,msgs)}await THREAD.save();await renderThreads();alert('Thread imported.')}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
||||||
|
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
|
||||||
|
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}$(window).on('resize orientationchange',()=>setTimeout(kbUpdate,50));$(el.input).on('focus click',()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)})}
|
||||||
|
function activeMeta(){return {sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar}}
|
||||||
|
const USER=window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await THREAD.persist()},logMany:async msgs=>{if(!Array.isArray(msgs)||!msgs.length)return;const clean=msgs.map(s=>String(s??'').trim()).filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]);const newMsgs=clean.map(t=>({id:gid(),role:'user',content:[{type:'text',text:t}]}));state.messages.push(...newMsgs);const frag=document.createDocumentFragment();const newEls=newMsgs.map(m=>{const $row=_createMessageRow(m),bubble=$row.find('.msg-bubble')[0];bubble.dataset.mid=m.id;return{rowEl:$row[0],bubbleEl:bubble,message:m}});newEls.forEach(item=>frag.appendChild(item.rowEl));el.messages.appendChild(frag);queueMicrotask(()=>{newEls.forEach(item=>{renderMarkdown(item.bubbleEl,partsToText(item.message))});el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'});icons()});await THREAD.persist()},get PAT(){return this.githubToken},get name(){return localStorage.getItem('user_name')||'Anon'},set name(v){localStorage.setItem('user_name',v||'')},get avatar(){return localStorage.getItem('user_avatar')||''},set avatar(v){localStorage.setItem('user_avatar',v||'')},get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',['openai','google','claude'].includes(v)?v:'openrouter')},get apiKeyOpenRouter(){return localStorage.getItem('openrouter_api_key')||''},set apiKeyOpenRouter(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOpenAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOpenAI(v){localStorage.setItem('openai_api_key',v||'')},get apiKeyGoogle(){return localStorage.getItem('google_api_key')||''},set apiKeyGoogle(v){localStorage.setItem('google_api_key',v||'')},get apiKeyClaude(){return localStorage.getItem('claude_api_key')||''},set apiKeyClaude(v){localStorage.setItem('claude_api_key',v||'')},get apiKeyCloudflare(){return localStorage.getItem('cloudflare_api_key')||''},set apiKeyCloudflare(v){localStorage.setItem('cloudflare_api_key',v||'')},get apiKey(){const p=this.provider;return p==='openai'?this.apiKeyOpenAI:p==='google'?this.apiKeyGoogle:p==='claude'?this.apiKeyClaude:p==='cloudflare'?this.apiKeyCloudflare:this.apiKeyOpenRouter},set apiKey(v){const p=this.provider;if(p==='openai')this.apiKeyOpenAI=v;else if(p==='google')this.apiKeyGoogle=v;else if(p==='claude')this.apiKeyClaude=v;else if(p==='cloudflare')this.apiKeyCloudflare=v;else this.apiKeyOpenRouter=v},get masterPrompt(){return localStorage.getItem('master_prompt')||'Always respond using markdown.'},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')},get titleModel(){return localStorage.getItem('title_model')??'or:amazon/nova-micro-v1'},set titleModel(v){localStorage.setItem('title_model',v||'')},get githubToken(){return localStorage.getItem('gh_token')||''},set githubToken(v){localStorage.setItem('gh_token',v||'')},get gcpSA(){try{return JSON.parse(localStorage.getItem('gcp_sa_json')||'null')}catch{return null}},set gcpSA(v){localStorage.setItem('gcp_sa_json',v?JSON.stringify(v):'')}}
|
||||||
|
async function init(){const u=localStorage.getItem('thread_repo_url')||'';el.threadRepoInput.value=u;el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads();await Promise.allSettled(STICKY_SUNES.map(s=>SUNE.fetchDotSune(s)));renderSidebar();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
||||||
|
$(window).on('resize',()=>{hideThreadPopover();hideSunePopover()})
|
||||||
|
const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})}
|
||||||
|
el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html';
|
||||||
|
el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
|
||||||
|
const pullThreads=async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const info=parseGhUrl(u);try{const items=await ghApi(`${info.apiPath}?ref=${info.branch}`);if(!items){THREAD.list=[];await THREAD.save()}else{THREAD.list=items.map(i=>{if(i.type==='dir')return {id:i.name,title:i.name,type:'folder',updatedAt:0};if(i.type==='file'&&i.name.endsWith('.md'))return {id:i.path,title:i.name,type:'file',updatedAt:0};const d=deserializeThreadName(i.name);return d?{...d,status:'synced'}:null}).filter(Boolean);await THREAD.save()}await renderThreads()}catch(e){console.error('Auto-pull failed:',e)}};
|
||||||
|
$(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);if(state.currentThreadId){state.currentThreadId=null;clearChat()}el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);if(u.startsWith('gh://'))await pullThreads();else{await THREAD.load();await renderThreads()}});
|
||||||
|
$(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}});
|
||||||
|
$(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()});
|
||||||
|
$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t),msgs=await localforage.getItem('rem_t_'+t.id);if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs,null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
|
||||||
|
init()
|
||||||
|
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI'],User:['accountTabUser','accountPanelUser']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})}
|
||||||
|
function openAccountSettings(){el.set_provider.value=USER.provider||'openrouter';el.set_api_key_or.value=USER.apiKeyOpenRouter||'';el.set_api_key_oai.value=USER.apiKeyOpenAI||'';el.set_api_key_g.value=USER.apiKeyGoogle||'';el.set_api_key_claude.value=USER.apiKeyClaude||'';el.set_api_key_cf.value=USER.apiKeyCloudflare||'';el.set_master_prompt.value=USER.masterPrompt||'';el.set_title_model.value=USER.titleModel;el.set_gh_token.value=USER.githubToken||'';const sa=USER.gcpSA;el.gcpSAUploadBtn.textContent=sa&&sa.project_id?`Uploaded: ${sa.project_id}`:'Upload .json';el.set_user_name.value=USER.name;el.userAvatarPreview.src=USER.avatar||'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';el.userAvatarPreview.classList.toggle('bg-gray-200',!USER.avatar);showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')}
|
||||||
|
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
|
||||||
|
$(el.accountSettingsOption).on('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
||||||
|
$(el.closeAccountSettings).on('click',closeAccountSettings)
|
||||||
|
$(el.cancelAccountSettings).on('click',closeAccountSettings)
|
||||||
|
$(el.accountSettingsModal).on('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()})
|
||||||
|
$(el.accountSettingsForm).on('submit',e=>{e.preventDefault();USER.provider=el.set_provider.value||'openrouter';USER.apiKeyOpenRouter=String(el.set_api_key_or.value||'').trim();USER.apiKeyOpenAI=String(el.set_api_key_oai.value||'').trim();USER.apiKeyGoogle=String(el.set_api_key_g.value||'').trim();USER.apiKeyClaude=String(el.set_api_key_claude.value||'').trim();USER.apiKeyCloudflare=String(el.set_api_key_cf.value||'').trim();USER.masterPrompt=String(el.set_master_prompt.value||'').trim();USER.titleModel=String(el.set_title_model.value||'').trim();USER.githubToken=String(el.set_gh_token.value||'').trim();USER.name=String(el.set_user_name.value||'').trim();closeAccountSettings()})
|
||||||
|
el.gcpSAUploadBtn.onclick=()=>el.gcpSAInput.click();el.gcpSAInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const t=await f.text(),d=JSON.parse(t);if(!d.project_id)throw new Error('Invalid');USER.gcpSA=d;el.gcpSAUploadBtn.textContent=`Uploaded: ${d.project_id}`;alert('GCP SA loaded.')}catch{alert('Failed to load GCP SA.')}};$(el.accountPanelAPI).on('click',e=>{const b=e.target.closest('[data-reveal-for]');if(!b)return;const i=document.getElementById(b.dataset.revealFor);if(!i)return;const p=i.type==='password';i.type=p?'text':'password';b.querySelector('i').setAttribute('data-lucide',p?'eye-off':'eye');lucide.createIcons()});
|
||||||
|
el.accountTabGeneral.onclick=()=>showAccountTab('General');el.accountTabAPI.onclick=()=>showAccountTab('API');el.accountTabUser.onclick=()=>showAccountTab('User')
|
||||||
|
el.setUserAvatarBtn.onclick=()=>el.userAvatarInput.click();el.userAvatarInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const dataUrl=await imgToWebp(f);USER.avatar=dataUrl;el.userAvatarPreview.src=dataUrl;el.userAvatarPreview.classList.remove('bg-gray-200')}catch{alert('Failed to process image.')}}
|
||||||
|
el.exportAccountSettings.onclick=()=>dl(`sune-account-${ts()}.json`,{v:1,provider:USER.provider,apiKeyOpenRouter:USER.apiKeyOpenRouter,apiKeyOpenAI:USER.apiKeyOpenAI,apiKeyGoogle:USER.apiKeyGoogle,apiKeyClaude:USER.apiKeyClaude,apiKeyCloudflare:USER.apiKeyCloudflare,masterPrompt:USER.masterPrompt,titleModel:USER.titleModel,githubToken:USER.githubToken,gcpSA:USER.gcpSA,userName:USER.name,userAvatar:USER.avatar});
|
||||||
|
el.importAccountSettings.onclick=()=>{el.importAccountSettingsInput.value='';el.importAccountSettingsInput.click()};
|
||||||
|
el.importAccountSettingsInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const d=JSON.parse(await f.text());if(!d||typeof d!=='object')throw new Error('Invalid');const m={provider:'provider',apiKeyOpenRouter:'apiKeyOR',apiKeyOpenAI:'apiKeyOAI',apiKeyGoogle:'apiKeyG',apiKeyClaude:'apiKeyC',apiKeyCloudflare:'apiKeyCF',masterPrompt:'masterPrompt',titleModel:'titleModel',githubToken:'ghToken',name:'userName',avatar:'userAvatar',gcpSA:'gcpSA'};Object.entries(m).forEach(([p,k])=>{const v=d[p]??d[k];if(typeof v==='string'||(p==='gcpSA'&&typeof v==='object'&&v))USER[p]=v});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}};
|
||||||
|
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
||||||
|
async function syncActiveThread(){const id=THREAD.getLastAssistantMessageId();if(!id)return false;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnSend();state.busy=false;state.controller=null}return false}if(!state.busy){state.busy=true;state.controller={abort:()=>{const ws=new WebSocket(HTTP_BASE.replace('https','wss'));ws.onopen=function(){this.send(JSON.stringify({type:'stop',rid:id}));this.close()}}};setBtnStop()}const bubble=getBubbleById(id);if(!bubble)return false;const msgIdx=state.messages.findIndex(x=>x.id===id);const localText=msgIdx>=0?partsToText(state.messages[msgIdx]):(bubble.textContent||'');const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c,imgs)=>{const tempMsg={content:c,images:imgs};renderMarkdown(bubble,partsToText(tempMsg),{enhance:false});enhanceCodeBlocks(bubble,true);if(msgIdx>=0){state.messages[msgIdx].content=c;state.messages[msgIdx].images=imgs}else state.messages.push({id,role:'assistant',content:c,images:imgs,...activeMeta()});THREAD.persist();setBtnSend();state.busy=false;cacheStore.setItem(id,'done');state.controller=null;el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:state.messages.find(m=>m.id===id)}}))};if(!j||j.rid!==id){if(j&&j.error){const t=localText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const serverText=j.text||'',isDone=j.error||j.done||j.phase==='done';const finalText=(serverText.length>=localText.length||isDone)?serverText:localText;const display=partsToText({content:[{type:'text',text:finalText}],images:j.images});if(display)renderMarkdown(bubble,display,{enhance:false});if(isDone){if(finalText!==localText){finalise(finalText,[{type:'text',text:finalText}],j.images)}else{await cacheStore.setItem(id,'done');if(state.busy){setBtnSend();state.busy=false;state.controller=null}}return false}await cacheStore.setItem(id,'busy');return true}
|
||||||
|
let syncLoopRunning=false
|
||||||
|
async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1500))}finally{syncLoopRunning=false}}
|
||||||
|
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
|
||||||
|
$(document).on('visibilitychange',onForeground)
|
||||||
|
$(el.copySystemPrompt).on('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
||||||
|
$(el.pasteSystemPrompt).on('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}})
|
||||||
|
const getActiveHtmlParts=()=>!el.htmlEditor.classList.contains('hidden')?[el.htmlEditor,jars.html]:[el.extensionHtmlEditor,jars.extension]
|
||||||
|
$(el.copyHTML).on('click',async()=>{try{await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'')}catch{}})
|
||||||
|
$(el.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const[editor,jar]=getActiveHtmlParts();if(jar&&jar.updateCode)jar.updateCode(t);else if(editor)editor.textContent=t}catch{}})
|
||||||
|
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveHtmlParts,imgToWebp,cacheStore,ghApi,parseGhUrl,pullThreads});
|
||||||
1
src/parts/chat.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-0 border-b border-gray-200 hidden"></section><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" @click="if($event.target.closest('.msg-avatar')){document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')}"></div><div class="h-24"></div></main>
|
||||||
12
src/parts/footer.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
||||||
|
<div class="mx-auto w-full max-w-none px-0">
|
||||||
|
<form id="composer" class="group relative flex items-start gap-2 px-3">
|
||||||
|
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
|
||||||
|
<div class="flex flex-col gap-2 self-stretch justify-center">
|
||||||
|
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
|
||||||
|
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
|
||||||
|
</div>
|
||||||
|
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
13
src/parts/head.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>Sune</title>
|
||||||
|
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css"/>
|
||||||
|
<link rel="stylesheet" href="/src/style.css"/>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js"></script>
|
||||||
|
<script defer src="//unpkg.com/alpinejs"></script>
|
||||||
|
<script defer src="https://c.planetrenox.com/tracker.js"></script>
|
||||||
|
|
||||||
116
src/parts/modals.html
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<div id="threadPopover" class="menu-card hidden">
|
||||||
|
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
||||||
|
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
||||||
|
<button data-action="duplicate" class="menu-item"><i data-lucide="copy" class="h-4 w-4"></i><span>Duplicate</span></button>
|
||||||
|
<button data-action="copy_path" class="menu-item"><i data-lucide="link" class="h-4 w-4"></i><span>Copy Path (GH)</span></button>
|
||||||
|
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export thread (.json)</span></button>
|
||||||
|
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
|
||||||
|
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
|
||||||
|
</div>
|
||||||
|
<div id="sunePopover" class="menu-card hidden">
|
||||||
|
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
||||||
|
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
||||||
|
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
|
||||||
|
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
|
||||||
|
</div>
|
||||||
|
<div id="suneModal" class="hidden fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/30"></div>
|
||||||
|
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
|
||||||
|
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center gap-2"><input id="suneURL" type="text" placeholder="" class="flex-1 min-w-0 h-10 rounded-xl border-0 bg-gray-50 px-3 text-gray-400 placeholder:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:bg-white text-xs font-mono focus:text-black"/><button id="syncSune" class="p-1.5 rounded hover:bg-gray-100" aria-label="Refresh"><i data-lucide="refresh-cw" class="h-5 w-5"></i></button></div>
|
||||||
|
<form id="settingsForm" class="text-sm">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
|
||||||
|
<div id="panelModel" class="p-4 space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top K</label><input id="set_top_k" type="number" min="0" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Frequency Penalty <span class="text-gray-400">(-2–2)</span></label><input id="set_frequency_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-2">
|
||||||
|
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
|
||||||
|
<div><input id="set_json_output" type="checkbox" class="sr-only peer"><label for="set_json_output" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">JSON Output</label></div>
|
||||||
|
<div><input id="set_img_output" type="checkbox" class="sr-only peer"><label for="set_img_output" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">IMG Output</label></div>
|
||||||
|
<div><input id="set_hide_composer" type="checkbox" class="sr-only peer"><label for="set_hide_composer" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Hide composer</label></div>
|
||||||
|
<div><input id="set_ignore_master_prompt" type="checkbox" class="sr-only peer"><label for="set_ignore_master_prompt" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Ignore master prompt</label></div>
|
||||||
|
</div>
|
||||||
|
<div id="aspectRatioContainer" class="hidden pt-2 grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-medium mb-1 text-xs">Aspect Ratio</label>
|
||||||
|
<select id="set_aspect_ratio" class="w-full rounded-xl border border-gray-300 px-3 py-2 text-xs">
|
||||||
|
<option value="1:1">1:1 (Square)</option>
|
||||||
|
<option value="2:3">2:3 (Portrait)</option>
|
||||||
|
<option value="3:2">3:2 (Landscape)</option>
|
||||||
|
<option value="3:4">3:4</option>
|
||||||
|
<option value="4:3">4:3</option>
|
||||||
|
<option value="4:5">4:5</option>
|
||||||
|
<option value="5:4">5:4</option>
|
||||||
|
<option value="9:16">9:16 (Story)</option>
|
||||||
|
<option value="16:9">16:9 (Cinematic)</option>
|
||||||
|
<option value="21:9">21:9 (Ultra-wide)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-medium mb-1 text-xs">Resolution</label>
|
||||||
|
<select id="set_image_size" class="w-full rounded-xl border border-gray-300 px-3 py-2 text-xs">
|
||||||
|
<option value="1K">1K (Standard)</option>
|
||||||
|
<option value="2K">2K (High)</option>
|
||||||
|
<option value="4K">4K (Ultra)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="panelPrompt" class="p-4 space-y-4 hidden">
|
||||||
|
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">JSON Schema</label><pre id="jsonSchemaEditor" class="w-full h-48 p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre><p class="mt-1 text-xs text-gray-500">Requires "JSON Output" to be enabled. Value for <code>json_schema</code>.</p></div>
|
||||||
|
</div>
|
||||||
|
<div id="panelScript" class="p-1 hidden">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></button></div>
|
||||||
|
<div class="pt-0">
|
||||||
|
<pre id="htmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
|
||||||
|
<pre id="extensionHtmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5 hidden" contenteditable="plaintext-only" spellcheck="false" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex gap-2"><button type="button" id="copyHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Paste</button></div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Scripts also run. extension.html runs before index.html.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
||||||
|
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button> <div class="flex items-center justify-end gap-2"><button type="button" id="cancelSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
|
||||||
|
<div class="absolute inset-0 bg-black/30"></div>
|
||||||
|
<div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4">
|
||||||
|
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
||||||
|
<form id="accountSettingsForm" class="text-sm">
|
||||||
|
<div class="border-b flex text-xs font-medium"><button type="button" id="accountTabGeneral" class="flex-1 py-2 px-3 text-center border-b-2 border-black">General</button><button type="button" id="accountTabAPI" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">API</button><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div>
|
||||||
|
<div id="accountPanelGeneral" class="p-4 space-y-4">
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: to override.</p></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
||||||
|
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
|
||||||
|
</div>
|
||||||
|
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div><div><label class="block text-gray-700 font-medium mb-1">GCP Service Acct</label><input id="gcpSAInput" type="file" class="hidden" accept="application/json,.json"><button type="button" id="gcpSAUploadBtn" class="w-full text-left rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm hover:bg-gray-50 truncate">Upload .json</button><p class="mt-1 text-xs text-gray-500">Use: <code>USER.gcpSA</code></p></div></div></div>
|
||||||
|
<div id="accountPanelUser" class="p-4 space-y-4 hidden">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>
|
||||||
|
<input id="userAvatarInput" type="file" accept="image/*" class="hidden">
|
||||||
|
<div class="flex-1"><label for="set_user_name" class="block text-gray-700 font-medium mb-1">Username</label><input id="set_user_name" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Master"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
||||||
|
<div class="flex items-center gap-2"><button type="button" id="importAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Import</button><button type="button" id="exportAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Export</button></div>
|
||||||
|
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="importAccountSettingsInput" type="file" class="hidden" accept="application/json,.json">
|
||||||
29
src/parts/sidebars.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
|
||||||
|
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
|
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
||||||
|
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
|
<div class="p-3 border-t relative">
|
||||||
|
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" @click.stop="document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
|
||||||
|
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
|
||||||
|
<button id="accountSettingsOption" class="menu-item"><i data-lucide="settings" class="h-4 w-4"></i><span>Settings</span></button>
|
||||||
|
<button id="sunesImportOption" class="menu-item">Import sunes (.sune)</button>
|
||||||
|
<button id="sunesExportOption" class="menu-item">Export sunes (.sune)</button>
|
||||||
|
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
|
||||||
|
</div>
|
||||||
|
<input id="importInput" type="file" accept="application/json,.json,.sune" class="hidden"/>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div id="sidebarOverlayRight" class="fixed inset-0 z-40 bg-black/20 hidden" @click="$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"></div>
|
||||||
|
<aside id="sidebarRight" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
|
<div class="p-2 border-b flex flex-col gap-2">
|
||||||
|
<input id="threadRepoInput" type="text" placeholder="gh://owner/repo" class="w-full h-9 rounded-lg border-0 bg-gray-100 px-3 text-xs font-mono focus:ring-2 focus:ring-black focus:bg-white"/>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button id="threadBackBtn" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center hidden" title="Back"><i data-lucide="chevron-left" class="h-4 w-4"></i></button>
|
||||||
|
<button id="threadFolderBtn" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center hidden" title="New Folder"><i data-lucide="folder-plus" class="h-4 w-4"></i></button>
|
||||||
|
</div>
|
||||||
|
<button id="threadSyncBtn" class="px-3 py-1 rounded-lg bg-black text-white text-[10px] font-bold uppercase tracking-wider hover:bg-black/90 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="threadList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
|
</aside>
|
||||||
7
src/parts/topbar.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||||
|
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
||||||
|
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
||||||
|
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
|
||||||
|
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
1
src/sticky-sunes.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const STICKY_SUNES=['sune-org/store@main/marketplace.sune'];
|
||||||
67
src/streaming.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
export const HTTP_BASE='https://us.proxy.sune.chat/ws'
|
||||||
|
|
||||||
|
export const buildBody=()=>{
|
||||||
|
const {USER,SUNE,state,payloadWithSampling}=window;
|
||||||
|
const msgs=[];
|
||||||
|
|
||||||
|
const mPrompt = (USER.masterPrompt || '').trim();
|
||||||
|
if(mPrompt && !SUNE.ignore_master_prompt) {
|
||||||
|
msgs.push({role:'system', content: mPrompt});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sPrompt = (SUNE.system_prompt || '').trim();
|
||||||
|
if(sPrompt) {
|
||||||
|
msgs.push({role:'system', content: sPrompt});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.messages.filter(m=>m.role!=='system').forEach(m=>{
|
||||||
|
let content = Array.isArray(m.content) ? [...m.content] : [{type:'text',text:String(m.content||'')}];
|
||||||
|
|
||||||
|
// Filter out empty text parts which cause 400 errors on strict providers like Moonshot
|
||||||
|
content = content.filter(p => p.type !== 'text' || (p.text && p.text.trim().length > 0));
|
||||||
|
|
||||||
|
msgs.push({
|
||||||
|
role: m.role,
|
||||||
|
content: content,
|
||||||
|
...(m.images?.length ? {images: m.images} : {})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strip trailing empty assistant message (prevents 400 on models without prefill support)
|
||||||
|
// We keep the UI bubble in main.js, but the API never sees the empty placeholder.
|
||||||
|
if (msgs.length > 0) {
|
||||||
|
const last = msgs[msgs.length - 1];
|
||||||
|
if (last.role === 'assistant' && last.content.length === 0 && (!last.images || last.images.length === 0)) {
|
||||||
|
msgs.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,''),messages:msgs,stream:true});
|
||||||
|
if(SUNE.json_output){let s;try{s=JSON.parse(SUNE.json_schema||'null')}catch{s=null}if(s&&typeof s==='object'&&Object.keys(s).length>0){b.response_format={type:'json_schema',json_schema:s}}else{b.response_format={type:'json_object'}}}
|
||||||
|
b.reasoning={...(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default'?{effort:SUNE.reasoning_effort}:{}),exclude:!SUNE.include_thoughts};
|
||||||
|
if(SUNE.verbosity)b.verbosity=SUNE.verbosity;
|
||||||
|
if(SUNE.img_output){b.modalities=['image'];b.image_config={aspect_ratio:SUNE.aspect_ratio||'1:1',image_size:SUNE.image_size||'1K'}}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
async function streamORP(body,onDelta,streamId){
|
||||||
|
const {USER,SUNE,state,gid,cacheStore}=window;
|
||||||
|
const model=SUNE.model,provider=model.startsWith('oai:')?'openai':model.startsWith('g:')?'google':model.startsWith('cla:')?'claude':model.startsWith('cf:')?'cloudflare':model.startsWith('or:')?'openrouter':USER.provider;
|
||||||
|
const apiKey=provider==='openai'?USER.apiKeyOpenAI:provider==='google'?USER.apiKeyGoogle:provider==='claude'?USER.apiKeyClaude:provider==='cloudflare'?USER.apiKeyCloudflare:USER.apiKeyOpenRouter;
|
||||||
|
if(!apiKey){onDelta(window.localDemoReply(),true);return {ok:true,rid:streamId||null}}
|
||||||
|
const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};
|
||||||
|
await cacheStore.setItem(r.rid,'busy');
|
||||||
|
const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};
|
||||||
|
const ws=new WebSocket(HTTP_BASE.replace('https','wss')+'?uid='+encodeURIComponent(r.rid));
|
||||||
|
r.ws=ws;
|
||||||
|
ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider,apiKey,or_body:body}));
|
||||||
|
ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false,m.images)}else if(m.type==='done'||m.type==='err'){r.done=true;cacheStore.setItem(r.rid,'done');signal(m.type==='err'?'\n\n'+(m.message||'error'):'');ws.close()}};
|
||||||
|
ws.onclose=()=>{};ws.onerror=()=>{};
|
||||||
|
state.controller={abort:()=>{r.done=true;cacheStore.setItem(r.rid,'done');try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{};signal('')},disconnect:()=>ws.close()};
|
||||||
|
return {ok:true,rid:r.rid}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function streamChat(onDelta,streamId){
|
||||||
|
const body=buildBody();
|
||||||
|
return await streamORP(body,onDelta,streamId)
|
||||||
|
}
|
||||||
28
src/style.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@import url(https://fonts.bunny.net/css?family=assistant:500);
|
||||||
|
:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
|
||||||
|
html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif}
|
||||||
|
.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
|
||||||
|
.markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em}
|
||||||
|
.msg-bubble{overflow-x:auto}
|
||||||
|
.msg-avatar{font-size:16px}
|
||||||
|
.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
|
||||||
|
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:0.875rem;display:flex;align-items:center;gap:.5rem}
|
||||||
|
#htmlEditor,#extensionHtmlEditor,#jsonSchemaEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5;}
|
||||||
|
:not(pre)>code{font-size:85%;padding:.2em .4em;margin:0;border-radius:6px;background-color:rgba(175,184,193,0.2)}
|
||||||
|
#threadRepoInput::placeholder{font-family:sans-serif;font-weight:500;color:#9ca3af}
|
||||||
|
|
||||||
|
/* MathJax 3 SVG Scaling & Alignment */
|
||||||
|
mjx-container[jax="SVG"] {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 0.125em !important;
|
||||||
|
}
|
||||||
|
mjx-container[jax="SVG"][display="true"] {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1em 0 !important;
|
||||||
|
}
|
||||||
|
mjx-container svg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
21
src/sune-logo.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const SUNE_LOGO_SVG = `
|
||||||
|
<div class="flex items-center justify-start py-1 opacity-80">
|
||||||
|
<style>
|
||||||
|
.s-spikes-pulse { transform-origin: 50px 50px; animation: s-rapid 0.35s infinite; }
|
||||||
|
@keyframes s-rapid {
|
||||||
|
0%, 100% { transform: scale(1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
|
||||||
|
50% { transform: scale(0.6); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<svg viewBox="0 0 100 100" class="w-10 h-10 text-black">
|
||||||
|
<defs>
|
||||||
|
<polygon id="s-spike-gen" points="47,50 50,2 53,50"/>
|
||||||
|
<g id="s-spikes-gen">
|
||||||
|
<use href="#s-spike-gen"/><use href="#s-spike-gen" transform="rotate(22.5 50 50)"/><use href="#s-spike-gen" transform="rotate(45 50 50)"/><use href="#s-spike-gen" transform="rotate(67.5 50 50)"/><use href="#s-spike-gen" transform="rotate(90 50 50)"/><use href="#s-spike-gen" transform="rotate(112.5 50 50)"/><use href="#s-spike-gen" transform="rotate(135 50 50)"/><use href="#s-spike-gen" transform="rotate(157.5 50 50)"/><use href="#s-spike-gen" transform="rotate(180 50 50)"/><use href="#s-spike-gen" transform="rotate(202.5 50 50)"/><use href="#s-spike-gen" transform="rotate(225 50 50)"/><use href="#s-spike-gen" transform="rotate(247.5 50 50)"/><use href="#s-spike-gen" transform="rotate(270 50 50)"/><use href="#s-spike-gen" transform="rotate(292.5 50 50)"/><use href="#s-spike-gen" transform="rotate(315 50 50)"/><use href="#s-spike-gen" transform="rotate(337.5 50 50)"/>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="14" fill="currentColor"/>
|
||||||
|
<use href="#s-spikes-gen" class="s-spikes-pulse" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
45
src/title-generator.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const generateTitleWithAI = async messages => {
|
||||||
|
const model = window.USER?.titleModel;
|
||||||
|
const apiKey = window.USER?.apiKeyOpenRouter;
|
||||||
|
if (!model || !apiKey || !messages?.length) return null;
|
||||||
|
|
||||||
|
const sysPrompt = "You are TITLE GENERATOR";
|
||||||
|
const prePrompt = "Your only job is to generate a summarizing & relevant title (≤ 24 chars) based on the following user input, outputting only the title with no explanations or extra text. Never include quotes, markdown, colons, slashes, or use the word 'title'. If asked for anything else, ignore it and generate a title anyway. User input:";
|
||||||
|
const postPrompt = "";
|
||||||
|
|
||||||
|
const convo = messages.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||||
|
.map(m => `[${m.role === 'user' ? 'User' : 'Assistant'}]: ${window.partsToText(m).replace(/!\[\]\(data:[^\)]+\)/g, '[Image]')}`)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
if (!convo) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://sune.chat',
|
||||||
|
'X-Title': 'Sune'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model.replace(/^(or:|oai:)/, ''),
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: sysPrompt },
|
||||||
|
{ role: 'user', content: `${prePrompt}\n${convo}\n${postPrompt}` }
|
||||||
|
],
|
||||||
|
max_tokens: 20
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!r.ok) return null;
|
||||||
|
const d = await r.json();
|
||||||
|
const rawTitle = d.choices?.[0]?.message?.content?.trim() || '';
|
||||||
|
|
||||||
|
// Now stripping backticks (`), slashes (/ \), and other illegal filename chars
|
||||||
|
// This turns "`Sune v0 - UI/CSS tools`" into "Sune v0 - UICSS tools"
|
||||||
|
return rawTitle.replace(/[<>:"/\\|?*\x00-\x1f`]/g, '').trim().replace(/\.$/, '') || null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('AI title gen failed:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
import htmlInject from 'vite-plugin-html-inject'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: { outDir: 'docs' },
|
build:{ minify:false },
|
||||||
plugins:[
|
plugins:[
|
||||||
|
htmlInject(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType:'autoUpdate',
|
registerType:'autoUpdate',
|
||||||
manifest:{
|
manifest:{
|
||||||
@@ -17,13 +19,8 @@ export default defineConfig({
|
|||||||
theme_color:'#FFFFFF',
|
theme_color:'#FFFFFF',
|
||||||
background_color:'#000000',
|
background_color:'#000000',
|
||||||
categories:['productivity','utilities'],
|
categories:['productivity','utilities'],
|
||||||
icons: [
|
icons:[{ src:'https://sune.planetrenox.com/appstore_content/✺.png', sizes:'1024x1024', type:'image/png' }],
|
||||||
{ src: 'https://sune.planetrenox.com/appstore_content/✺.png', sizes: '1024x1024', type: 'image/png' }
|
screenshots:[{ src:'https://sune.planetrenox.com/appstore_content/screenshot1.jpg', sizes:'1344x2693', type:'image/jpeg' },{ src:'https://sune.planetrenox.com/appstore_content/screenshot2.jpg', sizes:'1344x2699', type:'image/jpeg' }]
|
||||||
],
|
|
||||||
screenshots: [
|
|
||||||
{ src: 'https://sune.planetrenox.com/appstore_content/screenshot1.jpg', sizes: '1344x2693', type: 'image/jpeg' },
|
|
||||||
{ src: 'https://sune.planetrenox.com/appstore_content/screenshot2.jpg', sizes: '1344x2699', type: 'image/jpeg' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|||||||