Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2696b23f | ||
|
|
90e94994f5 | ||
|
|
2718915d34 | ||
|
|
8e0d755b57 | ||
|
|
ee83258b70 | ||
|
|
40542db400 | ||
|
|
f18994fd38 | ||
|
|
7c15ca6710 | ||
|
|
31cfa93408 | ||
|
|
b164e70541 | ||
|
|
3715af3138 | ||
|
|
973500a0f0 | ||
|
|
d9a94245fc | ||
|
|
1764d0d5f2 | ||
|
|
84370e2072 | ||
|
|
0289a76fce | ||
|
|
32e509b2b3 | ||
|
|
440b490b0d | ||
|
|
52b9cbf707 | ||
|
|
87505feac4 | ||
|
|
11326eeb1c | ||
|
|
485a8fa9d9 | ||
|
|
c5eefe60f0 | ||
|
|
4ce2f3a502 | ||
|
|
6e9e89a6cb | ||
|
|
5be21f2c3d | ||
|
|
90d29ed232 | ||
|
|
d9de41dd67 | ||
|
|
f1245da3e4 | ||
|
|
38c3de39f0 | ||
|
|
33639e5b89 | ||
|
|
d488e65e49 | ||
|
|
676b00afb6 | ||
|
|
0980eea975 | ||
|
|
eba3008482 | ||
|
|
9b904ac3dd | ||
|
|
704438b3d2 | ||
|
|
d35d075669 | ||
|
|
910cee9e67 | ||
|
|
dcc3e0799d | ||
|
|
21f3787b8b | ||
|
|
33b719d460 | ||
|
|
c6fcf51e0c | ||
|
|
62a3faa7c8 | ||
|
|
7cc6aca398 | ||
|
|
d87dfec2de | ||
|
|
5c0864bf0a | ||
|
|
8ce3a62826 | ||
|
|
8c3d6a02fc | ||
|
|
dd1064b3cb | ||
|
|
be9a490923 | ||
|
|
e053b36b6e | ||
|
|
9f29ecae9e | ||
|
|
2ea331f9ec | ||
|
|
296974e9e5 | ||
|
|
ed5b550ead | ||
|
|
4b86106a24 | ||
|
|
7f2c51957b | ||
|
|
043b0900d5 | ||
|
|
0c5e2e8cb0 | ||
|
|
40d06b781a | ||
|
|
c5328e6a35 | ||
|
|
babc5e95c6 | ||
|
|
644cabd23d | ||
|
|
1736227723 | ||
|
|
30943159bf | ||
|
|
c4d61288b4 | ||
|
|
389df2ace5 | ||
|
|
007ed5a7de | ||
|
|
ebc727e4fd | ||
|
|
32dbdca480 | ||
|
|
bc49df4561 | ||
|
|
28eb8f86a7 | ||
|
|
cdb8aa5f81 | ||
|
|
e818c24769 | ||
|
|
49babb6d25 | ||
|
|
b3aa9f4bac | ||
|
|
3785345353 | ||
|
|
daaa0eab26 | ||
|
|
541bc3280f | ||
|
|
b81d200eca | ||
|
|
51afebb394 | ||
|
|
1c0e4e9723 | ||
|
|
a5a9c952e8 | ||
|
|
4c9f75f31e | ||
|
|
d19d6ced66 | ||
|
|
7c34e18ad6 | ||
|
|
11f928bc95 | ||
|
|
e881c242fe | ||
|
|
8e77e2da1e | ||
|
|
e860357563 | ||
|
|
bc49ef6027 | ||
|
|
288ba64923 | ||
|
|
c69fde22bd | ||
|
|
0ce060693b | ||
|
|
db5b6ba574 | ||
|
|
be4851555d | ||
|
|
3afa76894f | ||
|
|
acf538ac01 | ||
|
|
5e1765d889 | ||
|
|
30799f17be | ||
|
|
dcfaa199bf | ||
|
|
1f961ee1df | ||
|
|
33b0af333c | ||
|
|
172250e6b5 | ||
|
|
15a46b903a | ||
|
|
6ff04ea4e3 | ||
|
|
0f667261be | ||
|
|
e412d7db3f | ||
|
|
6de3026f8f | ||
|
|
64067ea478 | ||
|
|
8af4fed89d | ||
|
|
e0b6e1cd19 | ||
|
|
64a5305bbf | ||
|
|
819e537aa3 | ||
|
|
68c4896088 | ||
|
|
771957429a | ||
|
|
5bf08c83a6 | ||
|
|
3ab2c9b282 | ||
|
|
94cf37a2ae | ||
|
|
47faa9bcdf | ||
|
|
5901246030 | ||
|
|
44e0b7596b | ||
|
|
178572bc7f | ||
|
|
70da7c5649 | ||
|
|
5c586652af | ||
|
|
067f602732 | ||
|
|
96bfdacee5 | ||
|
|
500dab29ab | ||
|
|
4d688ddbd8 | ||
|
|
8193cc53d5 | ||
|
|
a28ad2140e | ||
|
|
7b37415604 | ||
|
|
c4d244881f | ||
|
|
75963c9ad9 | ||
|
|
11e54550ed | ||
|
|
4f536d9b39 | ||
|
|
b527bc94bb | ||
|
|
54fd0e7316 | ||
|
|
6dbaf40f99 | ||
|
|
3aeb6ac5ce | ||
|
|
5a4881053d | ||
|
|
cebc44ff82 | ||
|
|
56044d9cb7 | ||
|
|
4809ee7db3 | ||
|
|
a4a1e73b8e | ||
|
|
5123b7bdd7 | ||
|
|
e189349eeb | ||
|
|
f2e81d8875 | ||
|
|
e5e0f3a7c3 | ||
|
|
c4fd9dd7da | ||
|
|
b4d076a2d4 | ||
|
|
bc82072710 | ||
|
|
bf023d746a | ||
|
|
fb5024ea82 | ||
|
|
c9c2dc7ea8 | ||
|
|
2a869f4ff6 | ||
|
|
af509eb670 | ||
|
|
783e698b6e | ||
|
|
ba912acf72 | ||
|
|
e0c09f6704 | ||
|
|
17a12bb7b6 | ||
|
|
f8757d1a43 | ||
|
|
69b95f0d6d | ||
|
|
ca32a22c0d | ||
|
|
3eb307c5c5 | ||
|
|
49af497085 | ||
|
|
8b732b43ee | ||
|
|
69e455274a | ||
|
|
e939dc4548 | ||
|
|
1ffead7ee2 | ||
|
|
a0a75a9964 | ||
|
|
37964d1f95 | ||
|
|
a9d66536c3 | ||
|
|
a968c3b5cd | ||
|
|
e52261fc3d | ||
|
|
4450f342d8 | ||
|
|
213dd841b5 | ||
|
|
816c46d447 | ||
|
|
db857ddd4d | ||
|
|
9be0c6555f | ||
|
|
e475c37f1d | ||
|
|
d86c9d48a6 | ||
|
|
008c9ede6e | ||
|
|
151ecceb85 | ||
|
|
1555d5344b | ||
|
|
bd8ec41914 | ||
|
|
1e21e559fb | ||
|
|
e496221dc3 | ||
|
|
5bf3955b26 | ||
|
|
1ab3329f6d | ||
|
|
e70bb4aa6f | ||
|
|
b22d0200c3 | ||
|
|
fb73ae0471 | ||
|
|
75aad4eef6 | ||
|
|
cec2996861 | ||
|
|
239b798c5d | ||
|
|
f1e53ef5c0 | ||
|
|
a368037e81 | ||
|
|
59f2f56124 | ||
|
|
3779bb69ef | ||
|
|
25153dbd84 | ||
|
|
91ba47bf47 | ||
|
|
28bf1ac7b2 | ||
|
|
45df500a9c | ||
|
|
f32de510fe | ||
|
|
21722b9f5a | ||
|
|
f49def09ea | ||
|
|
4948a80c56 | ||
|
|
7d249a81ce | ||
|
|
1d07025f74 | ||
|
|
91c5d43c48 | ||
|
|
b60d9ed09e | ||
|
|
56740ecf56 | ||
|
|
9f788e37de | ||
|
|
0634337858 | ||
|
|
08ce3dda73 | ||
|
|
c8f69781e4 | ||
|
|
13bfa719ee | ||
|
|
359bd28f2c | ||
|
|
7bd032ec77 | ||
|
|
790267ac43 | ||
|
|
d9844e0180 | ||
|
|
6ade3b69b9 | ||
|
|
ae184f434d | ||
|
|
055523bf33 | ||
|
|
866bc0f8d8 | ||
|
|
1bc203b5cd | ||
|
|
e2041c0b07 | ||
|
|
1cd99a5a90 | ||
|
|
2ca8a23102 | ||
|
|
97ce19973d | ||
|
|
783774d3f3 | ||
|
|
85e119db08 | ||
|
|
5436719bfb | ||
|
|
e571caaf2c | ||
|
|
5d2997b6a0 | ||
|
|
f8cb7be18c | ||
|
|
b93b82fcaf | ||
|
|
d0b4832ed7 | ||
|
|
4ffd5a3c94 | ||
|
|
2d6992d656 | ||
|
|
2fa7000ff7 | ||
|
|
bb7291a97c | ||
|
|
a699545c4c | ||
|
|
2c331aa744 | ||
|
|
82dd9fb08f | ||
|
|
b97aa77adc | ||
|
|
d7d101814f | ||
|
|
871d98e6f2 | ||
|
|
3cd093a8a2 | ||
|
|
4a59da7ff0 | ||
|
|
760b88d6f1 | ||
|
|
d7ad0624b7 | ||
|
|
27e6b0b23e | ||
|
|
55b5c2cc9f | ||
|
|
d5d5199f6d | ||
|
|
0bd0ac41cd | ||
|
|
b1c396717b | ||
|
|
388a9133fc | ||
|
|
221e947488 | ||
|
|
a1c63a1fe5 | ||
|
|
db2f21191c | ||
|
|
195ef3ee40 | ||
|
|
f3511e5cd0 | ||
|
|
96947d7636 | ||
|
|
034a379ecd | ||
|
|
da856f2b58 | ||
|
|
845c87e3ee | ||
|
|
7d9c0894f8 | ||
|
|
ae30a20739 | ||
|
|
7e7d3c9846 | ||
|
|
daff70c34f | ||
|
|
94188db9ea | ||
|
|
b12497898a | ||
|
|
3e51208024 | ||
|
|
36a3851233 | ||
|
|
54190d7a83 | ||
|
|
2215ec3c49 | ||
|
|
d21dbeb214 | ||
|
|
ba0c32dcee | ||
|
|
f5efe9b041 | ||
|
|
a4a44e1154 | ||
|
|
cef08638af | ||
|
|
025e43f4c5 | ||
|
|
fa04ab8b5a | ||
|
|
f3e5a3bb35 | ||
|
|
0547fb1715 | ||
|
|
bb6b9bd84b | ||
|
|
f485d4fff3 | ||
|
|
4bfaeb728b | ||
|
|
2b0ba2a68b | ||
|
|
c8cdd1d0b2 | ||
|
|
8fbfd35296 | ||
|
|
dfb745a5e4 | ||
|
|
c23350f17b | ||
|
|
124eb04a6b | ||
|
|
b4206d6421 | ||
|
|
537e2e4d06 | ||
|
|
9268c73776 | ||
|
|
6378cd33bd | ||
|
|
8c6d08d6bf | ||
|
|
29082b349d | ||
|
|
7d8656bfde | ||
|
|
45ce14e15a | ||
|
|
8947223255 | ||
|
|
5a420189b1 | ||
|
|
17650afe96 | ||
|
|
80fce34c1a | ||
|
|
ae31c50424 | ||
|
|
794aab1007 | ||
|
|
be06cac223 | ||
|
|
b0db8cc3d8 | ||
|
|
0dab5a8121 | ||
|
|
c012146c18 | ||
|
|
6851c96a45 | ||
|
|
f75f4f9db1 | ||
|
|
f22a6a9957 | ||
|
|
210cb5117f | ||
|
|
0c663f5f7c | ||
|
|
75e8a90ac7 | ||
|
|
d8af6c8f48 | ||
|
|
1ab4216ccb | ||
|
|
d39b58255d | ||
|
|
cd2ca406a5 | ||
|
|
637be00946 | ||
|
|
0dd93421c4 | ||
|
|
e27321cb23 | ||
|
|
6157b42f5a | ||
|
|
be4b180d92 | ||
|
|
e9e4c181b9 | ||
|
|
eb61b332a2 | ||
|
|
4fce6f9104 | ||
|
|
bcd656fb49 | ||
|
|
2d3033434b | ||
|
|
d522498cc9 | ||
|
|
65449ffd71 | ||
|
|
006c5bc4c1 | ||
|
|
a046d19ce1 | ||
|
|
aca089e8ef | ||
|
|
24eb629773 | ||
|
|
1847eb9d6a | ||
|
|
44d7304f8e | ||
|
|
5de74e90dc | ||
|
|
6fdca41a1d | ||
|
|
c978a25b7f | ||
|
|
c11307aa67 | ||
|
|
199269712f | ||
|
|
9572b66ecb | ||
|
|
45315f18d2 | ||
|
|
e889cc25cf | ||
|
|
8385d1913a | ||
|
|
9f462beec5 | ||
|
|
4f0c75e76e | ||
|
|
2097587b42 | ||
|
|
53af352ce8 | ||
|
|
0089af94d3 | ||
|
|
26ab6f663c | ||
|
|
bce0abba6e | ||
|
|
c732fd75e4 | ||
|
|
89232d7521 | ||
|
|
9ef8b1990e | ||
|
|
26f8010a57 | ||
|
|
abf97cdc78 | ||
|
|
4d10590511 | ||
|
|
3d60651491 | ||
|
|
a400989f64 | ||
|
|
2a1f50f726 | ||
|
|
158d157af4 | ||
|
|
1ed004fbb1 | ||
|
|
38f98cdf5c | ||
|
|
a417124c21 | ||
|
|
f3f11461de | ||
|
|
2e79cee24a | ||
|
|
1fd997303e | ||
|
|
cc017a22c7 | ||
|
|
5f01e8dd03 | ||
|
|
ce69905950 | ||
|
|
71e7b9dd63 | ||
|
|
58357547c9 | ||
|
|
fe7c6cb8f7 | ||
|
|
dc5b83e8cd | ||
|
|
27fffbec61 | ||
|
|
d35d121261 | ||
|
|
368673c3b4 | ||
|
|
854de9b43c | ||
|
|
0aa55eb333 | ||
|
|
1d1ab384ef | ||
|
|
864224e07c | ||
|
|
04b38a4a25 | ||
|
|
f8b40353ef | ||
|
|
893f1e6933 | ||
|
|
04e6d0aa00 | ||
|
|
1d6659f9e7 | ||
|
|
a4a67c1a00 | ||
|
|
8f773d3f1b | ||
|
|
c0305c9269 | ||
|
|
6989dd45f1 | ||
|
|
d8b43e7579 | ||
|
|
4f68235c94 | ||
|
|
c1143564ef | ||
|
|
ea2ea9783e | ||
|
|
e3e93712b6 | ||
|
|
afb78a6943 | ||
|
|
1a127ef602 | ||
|
|
acc29c8121 | ||
|
|
ca31490241 | ||
|
|
6c42f86e30 | ||
|
|
5b818836d7 | ||
|
|
3ea8e73389 | ||
|
|
c117e296cb | ||
|
|
42f55251f2 | ||
|
|
304e1b637a | ||
|
|
49173d9c28 | ||
|
|
8678ceb422 | ||
|
|
31cd19aabb | ||
|
|
60029ae3dc | ||
|
|
4f62703477 | ||
|
|
c0af23aefe | ||
|
|
6a6195f883 | ||
|
|
1a87077642 | ||
|
|
e9c8d1cea2 | ||
|
|
d2c76100f2 | ||
|
|
6aff165d09 | ||
|
|
5ecf28e60c | ||
|
|
fb103b6ad9 | ||
|
|
71a865dc0c | ||
|
|
8ed54448c2 | ||
|
|
5a2441cc9f | ||
|
|
089d3e171f | ||
|
|
ed71df1b60 | ||
|
|
85cd2c888e | ||
|
|
6673121f3f | ||
|
|
4898ea0a85 | ||
|
|
7d9a0637ca | ||
|
|
ba885ca201 | ||
|
|
ec60b77a71 | ||
|
|
ae3788a87d | ||
|
|
3e68260afa | ||
|
|
f551c7af9b | ||
|
|
b1ef4ccb1a | ||
|
|
e3631a9fd6 | ||
|
|
8241f8dd6b | ||
|
|
3d2cc9e5cd | ||
|
|
e263d86fc3 | ||
|
|
151384b26d | ||
|
|
030ede6887 | ||
|
|
e6ca2bf64f | ||
|
|
47cff9e2f9 | ||
|
|
6742346eb4 | ||
|
|
b5e682cfd2 | ||
|
|
2193c4484b | ||
|
|
4fc3d7dae2 | ||
|
|
6f0f9fb1fc | ||
|
|
df9c59e21c | ||
|
|
6131eda219 | ||
|
|
14082b3e7b | ||
|
|
00181ebde8 | ||
|
|
320f2ef814 | ||
|
|
8b022bffc5 | ||
|
|
5fddb08e04 | ||
|
|
0fedb6ffd8 | ||
|
|
498e3c6a84 | ||
|
|
85aa242fd5 | ||
|
|
6f94a40303 | ||
|
|
34893007a7 | ||
|
|
053d3e300e | ||
|
|
a1fbfaf864 | ||
|
|
b8c78ba06e | ||
|
|
cd33aaeded | ||
|
|
5aa66fc07d | ||
|
|
23d170cde1 | ||
|
|
d2be3b206d | ||
|
|
1a2ef0a537 | ||
|
|
ce3a5b6bf9 | ||
|
|
91a6183757 | ||
|
|
076cdc746e | ||
|
|
1c2e524b28 | ||
|
|
42ce20b4df | ||
|
|
1cbc0e2dd6 | ||
|
|
fc632f26e9 | ||
|
|
adabfa2fe3 | ||
|
|
ec56fa671b | ||
|
|
f144a59c97 | ||
|
|
dd942cdd60 | ||
|
|
fbec222eb4 | ||
|
|
7c55a577ad | ||
|
|
57ffe1a706 | ||
|
|
06b169d7a1 | ||
|
|
7487395e44 | ||
|
|
26557b30a0 | ||
|
|
f8228847ab | ||
|
|
6b480fdbe9 | ||
|
|
0a456e9146 | ||
|
|
b59cbd51d9 | ||
|
|
23743ee193 | ||
|
|
7748661963 | ||
|
|
e28277b21b | ||
|
|
32476c7ad4 | ||
|
|
ab305b3514 | ||
|
|
16ba5deb0e | ||
|
|
2b6424b3ff | ||
|
|
79f1a004a0 | ||
|
|
b89933e60a | ||
|
|
548cfd1719 | ||
|
|
7d32b41e90 | ||
|
|
6e3896fed3 | ||
|
|
545ec178b8 | ||
|
|
6dd1f9c017 | ||
|
|
6b963fd918 | ||
|
|
f13a1bb7fe | ||
|
|
02115bccfe | ||
|
|
48744441ba | ||
|
|
5ae6261e82 | ||
|
|
a76e28e018 | ||
|
|
f63a1faf3b | ||
|
|
3d75040297 | ||
|
|
8447790c47 | ||
|
|
b5f11ad790 | ||
|
|
d383b0e9dd | ||
|
|
ae2706dad3 | ||
|
|
fba417918f | ||
|
|
b513a0d56c | ||
|
|
b1b1ff98f5 | ||
|
|
3ab674e438 | ||
|
|
9a1b674950 | ||
|
|
9278fd772f | ||
|
|
a8c06ca7dc | ||
|
|
762d3620c4 | ||
|
|
4a403c95c5 | ||
|
|
8ca16c74bc | ||
|
|
60626c384a | ||
|
|
551c748aa8 | ||
|
|
03c155f8a7 | ||
|
|
3b165d9ea5 | ||
|
|
a85d2480b4 | ||
|
|
a9f01bec79 | ||
|
|
e906a911b2 | ||
|
|
8cc2650e98 | ||
|
|
593daa91ec | ||
|
|
a6dd0fdd85 | ||
|
|
30cf0fed91 | ||
|
|
1f2a1aaa76 | ||
|
|
c1c23d9136 | ||
|
|
4b2a93a542 | ||
|
|
23e6685977 | ||
|
|
bc9936bb2b | ||
|
|
ef615bbfb6 | ||
|
|
d8f32690a3 | ||
|
|
31f9838095 | ||
|
|
13ef1942f2 | ||
|
|
2b58fe6c8b | ||
|
|
81ca13280b | ||
|
|
bb7283dcd9 | ||
|
|
0bd4524334 | ||
|
|
ce78cbf936 | ||
|
|
6aaa4c841e | ||
|
|
179a3f2b2f | ||
|
|
678035afed | ||
|
|
758a8fda09 | ||
|
|
3a315798e9 | ||
|
|
bd5623ebc5 | ||
|
|
6a8f118ad0 | ||
|
|
3758122cf7 | ||
|
|
03b6fb724c | ||
|
|
3aa629f0df | ||
|
|
e5ca717783 | ||
|
|
85cca36ddb | ||
|
|
29cc004b69 | ||
|
|
19028ba627 | ||
|
|
77aa895a70 | ||
|
|
f762a76ecf | ||
|
|
095469b8a9 | ||
|
|
16afa9bcbb | ||
|
|
b04b830039 | ||
|
|
a8da6c2632 | ||
|
|
62549ab976 | ||
|
|
32772b7448 | ||
|
|
ecb79ce49c | ||
|
|
500a562bb6 | ||
|
|
b5744a1378 | ||
|
|
b8b71523b5 | ||
|
|
cc06d3f4d2 | ||
|
|
0890b3c375 | ||
|
|
ed887f762e | ||
|
|
bb05f7b1de | ||
|
|
d3880f51b3 | ||
|
|
a2fcd85746 | ||
|
|
47bf0c2a1b | ||
|
|
85dc5466c5 | ||
|
|
c97d88d33f | ||
|
|
60cc6665ce | ||
|
|
d82973c8c8 | ||
|
|
fca39eeaee | ||
|
|
e6a41dbc97 | ||
|
|
6746fe4a78 | ||
|
|
25e4e3dbc9 | ||
|
|
af8db1d24c | ||
|
|
bb2e896fba | ||
|
|
0dca637645 | ||
|
|
54fff09e74 | ||
|
|
39d7dab3e2 | ||
|
|
fc7576fea3 | ||
|
|
6c47da82c4 | ||
|
|
392394b4c9 | ||
|
|
a2bb8c80a2 | ||
|
|
ba0ec20532 | ||
|
|
138a5adb1c | ||
|
|
a2d767e2db | ||
|
|
dd631d57d4 | ||
|
|
a3c8c50212 | ||
|
|
ef1a12ab74 | ||
|
|
1c8b5b23a1 | ||
|
|
8d0759123a | ||
|
|
318f6c0c0c | ||
|
|
d8102e8db8 | ||
|
|
5b610fd807 | ||
|
|
7afdf5b363 | ||
|
|
6c5a2cffe3 | ||
|
|
0bad0eb306 | ||
|
|
5f39879e4d | ||
|
|
342fa8265a | ||
|
|
1fa2338ac3 | ||
|
|
56e17f826c | ||
|
|
bc5c2fa3d4 | ||
|
|
44cb118f95 | ||
|
|
1ece2c5a4f | ||
|
|
2fce5e61c9 | ||
|
|
d5d0494928 | ||
|
|
b380575f16 | ||
|
|
c21a1127f8 | ||
|
|
e621ce88e6 | ||
|
|
761491ec41 | ||
|
|
a35ed77ff0 | ||
|
|
245ac969a7 | ||
|
|
ab21911696 | ||
|
|
380407ae2f | ||
|
|
c0e89044c7 | ||
|
|
4f0494a97a | ||
|
|
baf3fadce6 | ||
|
|
ecf068a61c | ||
|
|
ecaaec939d | ||
|
|
e3c1adb71a | ||
|
|
ce165ece69 | ||
|
|
63bddfdea7 | ||
|
|
208a02a912 | ||
|
|
f750b7f04d | ||
|
|
ba5aeefe98 | ||
|
|
d82b5ed884 | ||
|
|
12f50123dc | ||
|
|
4511c0dee7 | ||
|
|
f6809349d2 | ||
|
|
97da7dc87f | ||
|
|
45049975c2 | ||
|
|
0e4caa5a66 | ||
|
|
f39cffcb9a | ||
|
|
5182ea5d11 | ||
|
|
5ebbb82d66 | ||
|
|
e6662cf673 | ||
|
|
fe0dc8d1a5 | ||
|
|
8794c51f88 | ||
|
|
8d1669adad | ||
|
|
da3d38013e | ||
|
|
b2ef4ce89e | ||
|
|
f525c49267 | ||
|
|
838e986670 | ||
|
|
fa70b31873 | ||
|
|
3855c30edd | ||
|
|
c7dccf8c0b | ||
|
|
2526304394 | ||
|
|
e1da39253a | ||
|
|
6b49741e69 | ||
|
|
2f414c6e08 | ||
|
|
9ed5420381 | ||
|
|
69c8c14c91 | ||
|
|
88b1a330a6 | ||
|
|
9e481cdcaa | ||
|
|
db50493577 | ||
|
|
3907034a1d | ||
|
|
b1b2540dc0 | ||
|
|
5dc1ed5685 | ||
|
|
1ad8da5ef2 | ||
|
|
22e9f18d08 | ||
|
|
b6c9265e31 | ||
|
|
ee6ab38c37 | ||
|
|
fbd237531e | ||
|
|
49101ef0e8 | ||
|
|
8751ee7361 | ||
|
|
01012ff300 | ||
|
|
2a2bb4d28e | ||
|
|
ce04f14f58 | ||
|
|
16b36e2b9f | ||
|
|
c597941128 | ||
|
|
ab22b50faf | ||
|
|
7785091bae | ||
|
|
56449ed129 | ||
|
|
da2864f422 | ||
|
|
c702119e66 | ||
|
|
30d4f9667b | ||
|
|
cd3a71baab | ||
|
|
391409b6c0 | ||
|
|
25dbeb7a9a | ||
|
|
12360c6f3f | ||
|
|
61754acb87 | ||
|
|
634cb1413b | ||
|
|
2fa40c5e96 | ||
|
|
432c12fbac | ||
|
|
146c8e76f0 | ||
|
|
e9f44c8f0f | ||
|
|
f3202f95b6 | ||
|
|
d5f319d9cb | ||
|
|
74324f4585 | ||
|
|
21e53310f8 | ||
|
|
a7596001c5 | ||
|
|
93243298e1 | ||
|
|
ca098e821f | ||
|
|
00d6bd8595 | ||
|
|
f0c85c4795 | ||
|
|
74113b3e6e | ||
|
|
cca954d01c | ||
|
|
d825cc7cfe | ||
|
|
ffca4261d7 | ||
|
|
865a0bfaf6 | ||
|
|
595da9f339 | ||
|
|
44b4a4beba | ||
|
|
c66c359432 | ||
|
|
1214ea0d91 | ||
|
|
cfa05e01c4 | ||
|
|
6a71272b79 | ||
|
|
b9b55b4e5b | ||
|
|
3f2aef2925 | ||
|
|
7a8a29639f | ||
|
|
becfd9b211 | ||
|
|
c6b17d5946 | ||
|
|
e24e058ab2 | ||
|
|
e74ef4b7dc | ||
|
|
c2f6d4fd6f | ||
|
|
2862eee33b | ||
|
|
3ad920457a | ||
|
|
768c6860f5 | ||
|
|
369efc784b | ||
|
|
522327d807 | ||
|
|
915233c5b6 | ||
|
|
229830ee89 | ||
|
|
dd329e7a4f | ||
|
|
da56a53a9c | ||
|
|
3f014a6fe1 | ||
|
|
d9d5780ea7 | ||
|
|
9002ae157a | ||
|
|
f279613f5d | ||
|
|
b58e731c19 | ||
|
|
9d73f27146 | ||
|
|
81f2a48c6d | ||
|
|
57a87d6322 | ||
|
|
728a4b1170 | ||
|
|
9de85591bc | ||
|
|
086c280e5d | ||
|
|
da677f87d5 | ||
|
|
28098b3155 | ||
|
|
c268348e29 | ||
|
|
b33426416b | ||
|
|
cf3dd2d225 | ||
|
|
11bc442f10 | ||
|
|
41ce95cbf1 | ||
|
|
75258db0a2 | ||
|
|
df8bf895ef | ||
|
|
1b86373a54 | ||
|
|
a4a9a4d26e | ||
|
|
1a41ac97d2 | ||
|
|
55aab67383 | ||
|
|
76e2e67af8 | ||
|
|
b682ff5d08 | ||
|
|
2d74f8883e | ||
|
|
e7b51a30a3 | ||
|
|
0b98d18483 | ||
|
|
3d6f35391e | ||
|
|
10047de745 | ||
|
|
82600a3404 | ||
|
|
b73eb77b95 | ||
|
|
1fca24765a | ||
|
|
f107ebc46d | ||
|
|
f2ffae8a22 | ||
|
|
ea78fe9726 | ||
|
|
7d61bc09bc | ||
|
|
b1231fd981 | ||
|
|
bcc3d617a0 | ||
|
|
6890176c76 | ||
|
|
d12c50ea4e | ||
|
|
31525e79a8 | ||
|
|
c48317672c | ||
|
|
f3ca996116 | ||
|
|
5daa4b3e20 | ||
|
|
80e84de868 | ||
|
|
7efd811e75 | ||
|
|
5e68f81a1f | ||
|
|
bebe4feccf | ||
|
|
e684de30bd | ||
|
|
bd0d922737 | ||
|
|
1668c1da87 | ||
|
|
95dec0e72e | ||
|
|
5a33f66ad6 | ||
|
|
33276dae76 | ||
|
|
b0e48b6bda | ||
|
|
f0aec4cd7f | ||
|
|
5ed636bce4 | ||
|
|
fa6191b448 | ||
|
|
93ca273312 | ||
|
|
82d14351fc | ||
|
|
3845d5090b | ||
|
|
17bb3715d1 | ||
|
|
f28426063f | ||
|
|
6efe1b80c5 | ||
|
|
01696f75e9 | ||
|
|
e97017b297 | ||
|
|
22222308a1 | ||
|
|
7fb3870247 | ||
|
|
c57696cf80 | ||
|
|
0784f30e23 | ||
|
|
9af93ecde6 | ||
|
|
d9e3a87911 | ||
|
|
f01789a17e | ||
|
|
688514eab6 | ||
|
|
3d069179d6 | ||
|
|
f90a6e7082 | ||
|
|
406c02ad07 | ||
|
|
3bc824b462 | ||
|
|
c705e0b5ae | ||
|
|
3a04ba5726 | ||
|
|
60a6db8b47 | ||
|
|
cf5550ab0a | ||
|
|
19aeba5fab | ||
|
|
d9afd8d887 | ||
|
|
7422f083b4 | ||
|
|
06508cfb7c | ||
|
|
980d8ba44f | ||
|
|
d658b05ee9 | ||
|
|
123fe36ee4 | ||
|
|
4bf4b96fa9 | ||
|
|
b8680275e5 | ||
|
|
e03ca7b4bd | ||
|
|
3178b4c45e | ||
|
|
413f92e270 | ||
|
|
bd426ed1d3 | ||
|
|
ca9960c8bc | ||
|
|
6b9e1f3c2a | ||
|
|
b8d3a549df | ||
|
|
546dec67a1 | ||
|
|
d61aa853f1 | ||
|
|
96e0d695c0 | ||
|
|
8fbbfc8093 | ||
|
|
853a349265 | ||
|
|
621d4c82af | ||
|
|
87e0ae683e | ||
|
|
44c0f6189b | ||
|
|
64afa9a1ed | ||
|
|
1ce15fbb15 | ||
|
|
ace30af202 | ||
|
|
559340143e | ||
|
|
ae29bb4ca8 | ||
|
|
f6af9379b7 | ||
|
|
57e286b8ad | ||
|
|
7acedfd385 | ||
|
|
946799d19d | ||
|
|
7282a32859 | ||
|
|
5080f1dc39 | ||
|
|
cba165933c | ||
|
|
e54f42c0eb | ||
|
|
c493639119 | ||
|
|
cc5ada1f3d | ||
|
|
539a8bcec8 | ||
|
|
4416348c51 | ||
|
|
bd0eceda61 | ||
|
|
dddd3c1670 | ||
|
|
10795d55e6 | ||
|
|
230fab013a | ||
|
|
2082d20b88 | ||
|
|
c230f7a6c6 | ||
|
|
3405795371 | ||
|
|
b70ed38d51 | ||
|
|
70bb2783b9 | ||
|
|
d9fc7c9fdf | ||
|
|
1938c80061 | ||
|
|
76320ca5a3 | ||
|
|
4aef81284a | ||
|
|
e7ff07debd | ||
|
|
df3fac1032 | ||
|
|
92a46003f8 | ||
|
|
24db448cfd | ||
|
|
9e14c2e289 | ||
|
|
e750df3c0d | ||
|
|
81e4fd92ce | ||
|
|
683cc6d76e | ||
|
|
fb692a0555 | ||
|
|
40206dc94d | ||
|
|
50d70a5050 | ||
|
|
8463289f75 | ||
|
|
b101ee7fc9 | ||
|
|
d06c8d49d7 | ||
|
|
e193c2df21 | ||
|
|
cc99ebba81 | ||
|
|
80d2db6315 | ||
|
|
c05e7df48e | ||
|
|
834df64650 | ||
|
|
8113b822c5 | ||
|
|
a53e565e5a | ||
|
|
d4a71ce384 | ||
|
|
004853f030 | ||
|
|
37be59b0dd | ||
|
|
273503ddb9 | ||
|
|
ce431ee155 | ||
|
|
12844e341c | ||
|
|
eaf1e075f2 | ||
|
|
7affaaf7c0 | ||
|
|
90f3564d0b | ||
|
|
05d14bb364 | ||
|
|
7b8c11852e | ||
|
|
5164fd362c | ||
|
|
2e689e4482 | ||
|
|
1ee2e6ca0a | ||
|
|
924c8f2fac | ||
|
|
0001f86826 | ||
|
|
9c9c912657 | ||
|
|
024c8ee5a1 | ||
|
|
2e7985639e | ||
|
|
e7c745a512 | ||
|
|
1b75887615 | ||
|
|
a1bcc6eed4 | ||
|
|
0bf6285287 | ||
|
|
4df71f32a8 | ||
|
|
44b7f9fa99 | ||
|
|
03329387b3 | ||
|
|
e5ba898fc7 | ||
|
|
a41b5a6d4c | ||
|
|
cbbde887d4 | ||
|
|
89724d6e4d | ||
|
|
6d5325d444 | ||
|
|
db1b32612b | ||
|
|
f4ccef8c70 | ||
|
|
99aa064e6c | ||
|
|
24a5ca0c06 | ||
|
|
6eda572f31 | ||
|
|
7b6dd9ba6f | ||
|
|
1341767692 | ||
|
|
f845805706 | ||
|
|
b2132dd668 | ||
|
|
fc5eaa2941 | ||
|
|
045bb11abf | ||
|
|
9a31155581 | ||
|
|
9e510ce088 | ||
|
|
a642ba91fa | ||
|
|
b0fdeab995 | ||
|
|
1bf1fc6512 | ||
|
|
392373b4b7 | ||
|
|
f3def88342 | ||
|
|
690d09a3d1 | ||
|
|
9471e72171 | ||
|
|
9ebde3b315 | ||
|
|
eaa061dd06 | ||
|
|
5f6ab41798 | ||
|
|
9adb0fee28 | ||
|
|
c577833e7c | ||
|
|
d57bcd9aa2 | ||
|
|
0e7149eec3 | ||
|
|
e53c1a0f7b | ||
|
|
e689493723 | ||
|
|
e6d3226c47 | ||
|
|
2c2650ff07 | ||
|
|
603d667608 | ||
|
|
e9125b0900 | ||
|
|
d0c696e523 | ||
|
|
6f9afcdba2 | ||
|
|
705de81c78 | ||
|
|
335bc3cf48 | ||
|
|
857798ff2f | ||
|
|
3955bd72b5 | ||
|
|
b5d80025e9 | ||
|
|
5b5e4f8ff0 | ||
|
|
ad5bd22c5e | ||
|
|
b2ed6f1acf | ||
|
|
2a8364e902 | ||
|
|
23a18ec318 | ||
|
|
d713f0209d | ||
|
|
0a21d37146 | ||
|
|
c645142f82 | ||
|
|
0e7a1de4db | ||
|
|
ef3ef433c1 | ||
|
|
0da91be9b1 | ||
|
|
69454ada69 | ||
|
|
1126fedea8 | ||
|
|
69c28e3563 | ||
|
|
b117bb1dc2 | ||
|
|
9c9330114f | ||
|
|
0fd314632a | ||
|
|
0d91c74289 | ||
|
|
d59f9e218f | ||
|
|
9bbd04d6cf | ||
|
|
22be6325a8 | ||
|
|
e96cc97462 | ||
|
|
92f5cb8fe3 | ||
|
|
a5897caec4 | ||
|
|
88c83b4dd7 | ||
|
|
fe0ede178e | ||
|
|
cca3d8eb41 | ||
|
|
5590f7d85a | ||
|
|
90bced3ed2 | ||
|
|
19399482e1 | ||
|
|
7657c45aae | ||
|
|
e6e0832c8a | ||
|
|
8f80b7c1ef | ||
|
|
c766e3fa3b | ||
|
|
788118ce3d | ||
|
|
77f8ded1e6 | ||
|
|
9d863f63e5 | ||
|
|
1e4ef070cf | ||
|
|
c1336ad5b3 | ||
|
|
143e0478ef | ||
|
|
cbd07c9d1b | ||
|
|
e3b0958c23 | ||
|
|
b843ab6542 | ||
|
|
dccce9355d | ||
|
|
6c30e649e3 | ||
|
|
bcebc8d5fb | ||
|
|
225398972d | ||
|
|
87f764bfa9 | ||
|
|
47217e9158 | ||
|
|
10334ddfe1 | ||
|
|
002e71f971 | ||
|
|
1f2b1e3b8a | ||
|
|
5d12616211 | ||
|
|
9704eff9f7 | ||
|
|
362a8268e8 | ||
|
|
c0e3c5d692 | ||
|
|
94c02ded2a | ||
|
|
998bd0ce06 | ||
|
|
6b6465b5e3 | ||
|
|
57da50b8d6 | ||
|
|
f87f6031de | ||
|
|
5b686824c5 | ||
|
|
95487d362f | ||
|
|
f496546cc0 | ||
|
|
9423d7e892 | ||
|
|
062f984262 | ||
|
|
925675ab54 | ||
|
|
c05513ae1e | ||
|
|
f257ccb682 | ||
|
|
d5b0bd4d31 | ||
|
|
5026bd1887 | ||
|
|
b0b0cf7b9e | ||
|
|
bf96cf4cb4 | ||
|
|
aeef1dca59 | ||
|
|
a1d460e220 | ||
|
|
e2a962de32 | ||
|
|
dbf1f46fa1 | ||
|
|
ce07a27b8d | ||
|
|
8f6e08eb2f | ||
|
|
a4b14e4911 | ||
|
|
56114562cb | ||
|
|
ba7834f222 | ||
|
|
d49406d2f1 | ||
|
|
a81d8adc90 | ||
|
|
3eae4a0f45 | ||
|
|
016f921cf0 | ||
|
|
9c8230a867 | ||
|
|
aea7724cdb | ||
|
|
79d87ad661 | ||
|
|
ad4b27b7ca | ||
|
|
40ba81ee6a | ||
|
|
d0e71a8104 | ||
|
|
f2d7d43e38 | ||
|
|
d7cb19ee96 | ||
|
|
9d425a19e8 | ||
|
|
48f37df3d5 | ||
|
|
ab876fc7d8 | ||
|
|
712326387f | ||
|
|
133054e2dd | ||
|
|
adcc5a358b | ||
|
|
5c2379577b | ||
|
|
c5352f2ee7 | ||
|
|
5574364438 | ||
|
|
cb0b30690e | ||
|
|
8442e6ec8d | ||
|
|
8ba3a67401 | ||
|
|
f2dc03b51e | ||
|
|
5d39aa55bd | ||
|
|
22574d3ada | ||
|
|
0727b4564b | ||
|
|
6521218308 | ||
|
|
34a9f15cb4 | ||
|
|
a5b65f4087 | ||
|
|
f590f672be | ||
|
|
d9afaca51f | ||
|
|
3b5525a0a5 | ||
|
|
fdfb888be9 | ||
|
|
02eee4bd3e | ||
|
|
b257963551 | ||
|
|
fb43c036f6 | ||
|
|
9ae6184dc1 | ||
|
|
7efedc7b44 | ||
|
|
e1e9be8daa | ||
|
|
247ff86f2f | ||
|
|
89cb5ab2f1 | ||
|
|
c484481964 | ||
|
|
613cece224 | ||
|
|
5aa1fdca04 | ||
|
|
73700cca54 | ||
|
|
3c92e87156 | ||
|
|
0122d3e361 | ||
|
|
30410b0a93 | ||
|
|
5c5632967c | ||
|
|
d872f4e177 | ||
|
|
677faab273 | ||
|
|
d40ee80680 | ||
|
|
79dd6b9be0 | ||
|
|
49cfc687f9 | ||
|
|
f0ccd6fb09 | ||
|
|
372d4f7933 | ||
|
|
6621519f11 | ||
|
|
1777a0171b | ||
|
|
93e702f9ca | ||
|
|
0d8cdf3afb | ||
|
|
fa41d620fd | ||
|
|
93543919d4 | ||
|
|
001b04ac5a | ||
|
|
417ec01fb2 | ||
|
|
0c5938a5a3 | ||
|
|
2e6eff43ad | ||
|
|
c714351819 | ||
|
|
e9c56f8635 | ||
|
|
5f88efb2c9 | ||
|
|
e6af567206 | ||
|
|
43cbcd86be | ||
|
|
44931f8205 | ||
|
|
bc7622b0bd | ||
|
|
1a1d22d6ee | ||
|
|
ab9034633a | ||
|
|
c860ee85f7 | ||
|
|
16adb90653 | ||
|
|
a5d877207e | ||
|
|
685abb6aa9 | ||
|
|
7f0a0d92a3 | ||
|
|
e4983be9d7 | ||
|
|
e30b44dae1 | ||
|
|
0922c8299f | ||
|
|
8adb4d5cef | ||
|
|
5198f6c4df | ||
|
|
5d0bafd651 | ||
|
|
b749c78efa | ||
|
|
22b93e32eb | ||
|
|
c767566cb6 | ||
|
|
a2cc1832f2 | ||
|
|
169346cdbd | ||
|
|
ea50540bd5 | ||
|
|
8e909ef1c1 | ||
|
|
7ad42ccf16 | ||
|
|
633a350bfc | ||
|
|
23d6f9efc2 | ||
|
|
838cae0d2d | ||
|
|
24e92c437d | ||
|
|
7d45c89d72 | ||
|
|
8e931c5acc | ||
|
|
be37779472 | ||
|
|
27cfd53c06 | ||
|
|
00e74a5e45 | ||
|
|
c890ae5a15 | ||
|
|
786ddaa38f | ||
|
|
d162e5533f | ||
|
|
e68b726f9a | ||
|
|
8ed3b0eb91 | ||
|
|
a9332c0ab0 | ||
|
|
423abf4fde | ||
|
|
07ad87365f | ||
|
|
2c136c1fde | ||
|
|
1a122eb7fd | ||
|
|
4a63399363 | ||
|
|
ee8e2b3225 | ||
|
|
8aa52e7e3c | ||
|
|
de597b602a | ||
|
|
2371364911 | ||
|
|
214e794451 | ||
|
|
ab902f948a | ||
|
|
ce1e36f4fd | ||
|
|
72eb5204ff | ||
|
|
e4da5bec8d | ||
|
|
6102963624 | ||
|
|
b152cb43fc | ||
|
|
ebe1678b30 | ||
|
|
8a0962681a | ||
|
|
9778f9521e | ||
|
|
4a3cfb9dfa | ||
|
|
1cfa93e855 | ||
|
|
4fab13c728 | ||
|
|
a3c69dab07 | ||
|
|
e3dd430e68 | ||
|
|
d018ecd805 | ||
|
|
a4b7ab5e9d | ||
|
|
ef1481be63 | ||
|
|
3a2b5d5ccd | ||
|
|
78f6e37f76 | ||
|
|
1ea521abab | ||
|
|
b8a97f2138 | ||
|
|
dac5ff38db | ||
|
|
ac98d2f585 | ||
|
|
37a9cca82d | ||
|
|
8c53594df7 | ||
|
|
eec7567ac6 | ||
|
|
920fde9cd0 | ||
|
|
742bbdf3ac | ||
|
|
4da8a1301e | ||
|
|
3bd5cfab9d | ||
|
|
4f8b0f1c67 | ||
|
|
c59819f6dc | ||
|
|
96d0f4ae73 | ||
|
|
a35aeee6a2 | ||
|
|
2688cda6b5 | ||
|
|
f668133297 | ||
|
|
89bd07be92 | ||
|
|
a5fb806028 | ||
|
|
83714f628b | ||
|
|
ae3e2fb4e8 | ||
|
|
650102cedb | ||
|
|
61babbdf7d | ||
|
|
3734b6a05f | ||
|
|
0256d527f0 | ||
|
|
7adb93fb8e | ||
|
|
97f24f17eb | ||
|
|
96b0bae6ca | ||
|
|
3632b0d726 | ||
|
|
6792b4f4c4 | ||
|
|
10e35055e0 | ||
|
|
7548620683 | ||
|
|
d5e12e3506 | ||
|
|
23b9d401b7 | ||
|
|
44894765b6 | ||
|
|
c2568f4be9 | ||
|
|
90adb9dfa3 | ||
|
|
d2f3eba434 | ||
|
|
2fe058e0ee | ||
|
|
4410acaee8 | ||
|
|
d03f5fbbc0 | ||
|
|
ae1c2f95a7 | ||
|
|
381547ec78 | ||
|
|
f4672ea6b8 | ||
|
|
51a270a2f7 | ||
|
|
f3c8f2e1ab | ||
|
|
e2b676caaa | ||
|
|
68ac49259c | ||
|
|
d94c0aae4b | ||
|
|
047e700d09 | ||
|
|
9ccc473982 | ||
|
|
4142029f48 | ||
|
|
dbf2cc8d80 | ||
|
|
f200972c00 | ||
|
|
3033597f02 | ||
|
|
a16dd889cf | ||
|
|
dab6ecbf9a | ||
|
|
8bdbf6ab15 | ||
|
|
8f75e12871 | ||
|
|
36311e6cef | ||
|
|
8f5af46ec2 | ||
|
|
aeebf53739 | ||
|
|
1d81ee9ed0 | ||
|
|
e1c9d24aa4 | ||
|
|
b7da062a4c | ||
|
|
6fd6d564eb | ||
|
|
106f1d1794 | ||
|
|
3809d7f3e7 | ||
|
|
c769dd20f7 | ||
|
|
445865e422 | ||
|
|
24d4934c27 | ||
|
|
fe7cdba07e | ||
|
|
c56db15a03 | ||
|
|
deb9993a64 | ||
|
|
1ff8860b98 | ||
|
|
198fc18c50 | ||
|
|
621fdc9244 | ||
|
|
9ece6213a9 | ||
|
|
35c313b689 | ||
|
|
d6b88b842d | ||
|
|
ff90cac755 | ||
|
|
8f93606c81 | ||
|
|
95baa44a1c | ||
|
|
fa65e67069 | ||
|
|
066d5032f9 | ||
|
|
3f8a520624 | ||
|
|
6ab6695be7 | ||
|
|
eda0ccffdb | ||
|
|
1d399c3c21 | ||
|
|
cfd0faf5e7 | ||
|
|
ffee030548 | ||
|
|
9d81858ff4 | ||
|
|
f791210e70 | ||
|
|
794ff93fa9 | ||
|
|
ff8eb930f7 | ||
|
|
d71a21ca8b | ||
|
|
2bd23dd374 | ||
|
|
a501463ac7 | ||
|
|
9bd049923e | ||
|
|
67152f1c83 | ||
|
|
8ca3fdec75 | ||
|
|
8e7fc54548 | ||
|
|
1642c3e694 | ||
|
|
d738c13e9e | ||
|
|
45eb46c3f3 | ||
|
|
7b629d0697 | ||
|
|
89a1fa5042 | ||
|
|
c9c59b761f | ||
|
|
8b2f398015 | ||
|
|
5c5bf7a60e | ||
|
|
6cf4404243 | ||
|
|
56ff310f29 | ||
|
|
b671b49725 | ||
|
|
d3d7db1f34 | ||
|
|
26a388e3bf | ||
|
|
6a9d8e903e | ||
|
|
daba5c0599 | ||
|
|
d1e6cd5a8f | ||
|
|
eeb7f30585 | ||
|
|
29fdb06cea | ||
|
|
6d02e2c89e | ||
|
|
cf8c17d08b | ||
|
|
ad5d55220c | ||
|
|
2ecd50f5dc | ||
|
|
48d22e0d42 | ||
|
|
096e85fbc3 | ||
|
|
e3d8e534f6 | ||
|
|
acadc246cb | ||
|
|
fd2c8d1d64 | ||
|
|
f7575e4ddb | ||
|
|
114ee8f6ac | ||
|
|
b91a45639f | ||
|
|
c4277f8566 | ||
|
|
b7aa8b06ca | ||
|
|
26097b7cfe | ||
|
|
6addf507cb | ||
|
|
f0bf29ba08 | ||
|
|
6ef7701805 | ||
|
|
70814b1026 | ||
|
|
ca1d7e77ba | ||
|
|
a800baf8ae | ||
|
|
2f7f07467e | ||
|
|
f472bba9f0 | ||
|
|
48de418d21 | ||
|
|
8892637b97 | ||
|
|
8c55d4b55d | ||
|
|
dffb8a955b | ||
|
|
86e3b4de06 | ||
|
|
525218f9a7 | ||
|
|
9a70329db2 | ||
|
|
9a63a4b7ca | ||
|
|
357f245aec | ||
|
|
8f9451a24b | ||
|
|
6396ccc701 | ||
|
|
9361c069db | ||
|
|
0b5cc2d639 | ||
|
|
61358b6233 | ||
|
|
05815d9475 | ||
|
|
77db1a6297 | ||
|
|
21254a060a | ||
|
|
f049936ec4 | ||
|
|
8673eb5eca | ||
|
|
43df9175df | ||
|
|
7fcf22de90 | ||
|
|
6f9d81ca68 | ||
|
|
267ce81be2 | ||
|
|
3987ee7ee3 | ||
|
|
87c9986b33 | ||
|
|
a85f1571a0 | ||
|
|
d6ff00226a | ||
|
|
7f1f34e34f | ||
|
|
edb3238f3e | ||
|
|
67d5e43ec4 | ||
|
|
f8e3a3e5b4 | ||
|
|
ef410e298d | ||
|
|
80fc2a9240 | ||
|
|
3ae1202536 | ||
|
|
2c0bb2041b | ||
|
|
ab6b6876aa | ||
|
|
284445e0c9 | ||
|
|
05729505d8 | ||
|
|
4004fb908b | ||
|
|
19660e49d2 | ||
|
|
b6a2c9336e | ||
|
|
ba2bc46042 | ||
|
|
ad8f74c964 | ||
|
|
ded3123af2 | ||
|
|
e076abe759 | ||
|
|
c1bb4864ab | ||
|
|
b3a39cbdf7 | ||
|
|
d59e0514b3 | ||
|
|
6e88c5a1c5 | ||
|
|
c74be2b789 | ||
|
|
a258122e22 | ||
|
|
a01d7123d7 | ||
|
|
7652db4601 | ||
|
|
433c540ee2 | ||
|
|
37e1f7104f | ||
|
|
6467c8a870 | ||
|
|
a892f55cb2 | ||
|
|
ac4424bab2 | ||
|
|
99992c951d | ||
|
|
c516b74b75 | ||
|
|
da53b81bf9 | ||
|
|
f730924572 | ||
|
|
0fb69a38cd | ||
|
|
125e8b271b | ||
|
|
8c2ab6b860 | ||
|
|
d639b24c2d | ||
|
|
6ebbf3aa32 | ||
|
|
b97548f8d6 | ||
|
|
87eff3e133 | ||
|
|
2e6ce67b64 | ||
|
|
843eda6fea | ||
|
|
643f9b2c76 | ||
|
|
2089f5e8ee | ||
|
|
7e98d7dc52 | ||
|
|
a514b2fe79 | ||
|
|
d56230e04d | ||
|
|
03e0d8248d | ||
|
|
065bd8f28a | ||
|
|
ca13a9199f | ||
|
|
b59d476c2d | ||
|
|
c69486556a | ||
|
|
bfd75da308 | ||
|
|
53bbb9b15b | ||
|
|
f1d58983c2 | ||
|
|
a8a545690f | ||
|
|
3748513996 | ||
|
|
18c2a4122f | ||
|
|
04664c8d42 | ||
|
|
628bbe14a8 | ||
|
|
abae7c9d67 | ||
|
|
eeee00f6cd | ||
|
|
6971ea5f9c | ||
|
|
cee38f93c8 | ||
|
|
fa5a6b8176 | ||
|
|
97f8940b15 | ||
|
|
556d23ade5 | ||
|
|
313290b22c | ||
|
|
d339db4799 | ||
|
|
03dff2ce77 | ||
|
|
c2c44f8d29 | ||
|
|
13130c7f27 | ||
|
|
30b986bb33 | ||
|
|
0196b0a445 | ||
|
|
8edd06fb67 | ||
|
|
38b59cc198 | ||
|
|
4fbd0bb5c5 | ||
|
|
c2563661d0 | ||
|
|
f358cfb7c7 | ||
|
|
ef11d3e0ed | ||
|
|
f44e2ccf52 | ||
|
|
7e75c5b9b9 | ||
|
|
15ae11f966 | ||
|
|
226439ef36 | ||
|
|
81aca93b81 | ||
|
|
777c3b0079 | ||
|
|
ab85e4df72 | ||
|
|
cf8185498a | ||
|
|
bf34b7df4e | ||
|
|
028c5a34e6 | ||
|
|
c0ea0b0f5a | ||
|
|
4c38055d38 | ||
|
|
87fcbc5bfb | ||
|
|
043a1aab21 | ||
|
|
9f86c8e40d | ||
|
|
3330da0e59 | ||
|
|
64492bec43 | ||
|
|
2402a84c71 | ||
|
|
294d06f22b | ||
|
|
d6e788dc89 | ||
|
|
070cd07199 | ||
|
|
4ded0ccd4f | ||
|
|
6253a2bca9 | ||
|
|
bd1efd8026 | ||
|
|
4940e0d8ed | ||
|
|
8d6cc65530 | ||
|
|
b8adefbb88 | ||
|
|
2c56a84bb7 | ||
|
|
613e22d6ca | ||
|
|
73e76fc72f | ||
|
|
1e5e2581c8 | ||
|
|
b25c4e33cb | ||
|
|
17b28f43f8 | ||
|
|
e3cde9be25 | ||
|
|
449703a6f0 | ||
|
|
358d5c72a6 | ||
|
|
abd2ced4b4 | ||
|
|
49e5aaea6b | ||
|
|
d1f431d121 | ||
|
|
fd759d6845 | ||
|
|
5be5531722 | ||
|
|
2a4fccde70 | ||
|
|
d6f018e581 | ||
|
|
e0c1cb6581 | ||
|
|
f80a00e211 | ||
|
|
4aee413680 | ||
|
|
2a91e2e7ad | ||
|
|
dba821a70e | ||
|
|
836dd73446 | ||
|
|
8f88427d4e | ||
|
|
817585a4ce | ||
|
|
0386f54449 | ||
|
|
4761677676 | ||
|
|
f61d053010 | ||
|
|
8440d02198 | ||
|
|
ebcdf847d2 | ||
|
|
5c553aae19 | ||
|
|
0af8ca5f78 | ||
|
|
52ba178c4e | ||
|
|
166016a741 | ||
|
|
20786e690d | ||
|
|
c19417a706 | ||
|
|
b768e0affb | ||
|
|
05ff10df1b | ||
|
|
a0a6227368 | ||
|
|
918e65d8d3 | ||
|
|
6cab62d140 | ||
|
|
9526268f1f | ||
|
|
016ec1877a | ||
|
|
772e75f04d | ||
|
|
10d7190ea4 | ||
|
|
cc8703577d | ||
|
|
4533755704 | ||
|
|
da4a08dfe1 | ||
|
|
48ddbf0412 | ||
|
|
39def1c20b | ||
|
|
e8a31b5d37 | ||
|
|
5a25dd08f6 | ||
|
|
b34219ce86 | ||
|
|
05548b0fa6 | ||
|
|
e08e7be24c | ||
|
|
1be600155b | ||
|
|
be757b1f2f | ||
|
|
d592e1e535 | ||
|
|
e381e9f52e | ||
|
|
3b951efd13 | ||
|
|
33161c5bad | ||
|
|
8b6236c796 | ||
|
|
a59d80d7f3 | ||
|
|
a5c76591ac | ||
|
|
c2a975af9e | ||
|
|
ab48cd5b55 | ||
|
|
ed4909a28d | ||
|
|
afc517b3a7 | ||
|
|
de9f6b6711 | ||
|
|
646cb8caeb | ||
|
|
d533ddde88 | ||
|
|
9664cb653a | ||
|
|
1574d44cc3 | ||
|
|
6981fc06ca | ||
|
|
12798c00bc | ||
|
|
ebb6be816b | ||
|
|
ae4e4f67c2 | ||
|
|
013aafde17 | ||
|
|
050b1ad78e | ||
|
|
21a76f5e7c | ||
|
|
f9d7610679 | ||
|
|
aeba15eb16 | ||
|
|
b81f64a253 | ||
|
|
a0d04cba50 | ||
|
|
43bdbcf89c | ||
|
|
6947a3ffa7 | ||
|
|
62d525dae7 | ||
|
|
f8bc74b27f | ||
|
|
044370f13c | ||
|
|
a2c45492cb | ||
|
|
9a67ca7c03 | ||
|
|
40cd8a24af | ||
|
|
28948975db | ||
|
|
1ce1db9689 | ||
|
|
86cdeed6ef | ||
|
|
d6ec0afbd6 | ||
|
|
3ea33e1dee | ||
|
|
3e6e3c08d5 | ||
|
|
6155a2ec2b | ||
|
|
4e0787ec77 | ||
|
|
8ecc32da6d | ||
|
|
eb41da78f2 | ||
|
|
a508969331 | ||
|
|
99febda256 | ||
|
|
543d78e040 | ||
|
|
4ef96c7930 | ||
|
|
7a7c99f752 | ||
|
|
6b1f17d6a8 | ||
|
|
cf191ef874 | ||
|
|
f6d3d013fd | ||
|
|
e6ad3c414f | ||
|
|
8162719e36 | ||
|
|
f5bc02d598 | ||
|
|
139dfb76e4 | ||
|
|
f4769c4046 | ||
|
|
fea3cc741f | ||
|
|
4e764350a4 | ||
|
|
c63811367e | ||
|
|
4cc8b3198b | ||
|
|
5c6ac06656 | ||
|
|
9b6b94d919 | ||
|
|
8f4af5ece4 | ||
|
|
2d9248889f | ||
|
|
804f37dcfa | ||
|
|
bf98713a55 | ||
|
|
2f2ba44c7c | ||
|
|
62292500ba | ||
|
|
a7f1e89ce9 | ||
|
|
20c7e2841c | ||
|
|
c79f5c5a7a | ||
|
|
d3771ed31f | ||
|
|
230eec1baf | ||
|
|
0bae393e07 | ||
|
|
434cf40b36 | ||
|
|
7417a1c950 | ||
|
|
799ccbe300 | ||
|
|
43044232bb | ||
|
|
4567db8836 | ||
|
|
e286e59431 | ||
|
|
65e89a4248 | ||
|
|
db63086c38 | ||
|
|
2abdf28eba | ||
|
|
2476f04a26 | ||
|
|
e458f1eab4 | ||
|
|
b15b2649f8 | ||
|
|
4fce7ba09a | ||
|
|
1a1369f115 | ||
|
|
9434ffabdc | ||
|
|
6aeb428796 | ||
|
|
8cabf32ebe | ||
|
|
85a98fcfd6 | ||
|
|
aa1f81a30a | ||
|
|
1c50c73aa4 | ||
|
|
6cc28c7edd | ||
|
|
1c2cf768f8 | ||
|
|
481e718d86 | ||
|
|
29f74afbf6 | ||
|
|
02111ab5b8 | ||
|
|
a2f7157bc3 | ||
|
|
e6591abfbc | ||
|
|
50bda1ddee | ||
|
|
31abad86d5 | ||
|
|
af7e90c87a | ||
|
|
2e4bdf122e | ||
|
|
83d7fa26fe | ||
|
|
efc4d89fdf | ||
|
|
a27206c543 | ||
|
|
faca2c9a2d | ||
|
|
0cbd137b4f | ||
|
|
cdf41d3e5e | ||
|
|
aa034f8669 | ||
|
|
aee5a50b9f | ||
|
|
b8926f00d7 | ||
|
|
839386f0b4 | ||
|
|
cf2e4bdd9c | ||
|
|
e7a455f22e |
No files matched your search
@@ -0,0 +1,163 @@
|
|||||||
|
# Workspace/editor
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
frontend-web/.vscode/
|
||||||
|
node_modules/
|
||||||
|
# Claude Code local-only settings (per-user permissions, not shared)
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Local runtime logs and pid files
|
||||||
|
.runtime/
|
||||||
|
logs/
|
||||||
|
offline-backend-20260512/backend/logs/
|
||||||
|
|
||||||
|
# Frontend local/runtime artifacts
|
||||||
|
frontend-web/.env
|
||||||
|
frontend-web/.env.local
|
||||||
|
frontend-web/.env.*.local
|
||||||
|
frontend-web/node_modules/
|
||||||
|
frontend-web/.pnpm-store/
|
||||||
|
frontend-web/tsconfig.tsbuildinfo
|
||||||
|
frontend-web/vite-dev.log
|
||||||
|
frontend-web/vite-dev.err.log
|
||||||
|
|
||||||
|
# Frontend local packages/archives/docs (non-source)
|
||||||
|
frontend-web/pnpm-lock.yaml
|
||||||
|
frontend-web/README.md
|
||||||
|
frontend-web/doc.md
|
||||||
|
frontend-web/src.zip
|
||||||
|
frontend-web/src9.zip
|
||||||
|
frontend-web/src-v1.zip
|
||||||
|
frontend-web/src-*.rar
|
||||||
|
frontend-web/dist
|
||||||
|
# Frontend build artifacts
|
||||||
|
frontend-web/dist/
|
||||||
|
frontend-web/dist-scheduled-check/
|
||||||
|
|
||||||
|
# Offline backend bundles and local deployment artifacts
|
||||||
|
offline-backend-20260505.zip
|
||||||
|
offline-backend-20260505/output/
|
||||||
|
offline-backend-20260505/bundle/.env
|
||||||
|
offline-backend-20260505/bundle/.env.example
|
||||||
|
offline-backend-20260505/bundle/DEPLOY_BACKEND_OFFLINE_ZH.md
|
||||||
|
offline-backend-20260505/bundle/SHA256SUMS
|
||||||
|
offline-backend-20260505/bundle/config.yaml
|
||||||
|
offline-backend-20260505/bundle/config.yaml.example
|
||||||
|
offline-backend-20260505/bundle/data/
|
||||||
|
offline-backend-20260505/bundle/deerflow-backend-offline-image-20260505.tar
|
||||||
|
offline-backend-20260505/bundle/extensions_config.json
|
||||||
|
offline-backend-20260505/bundle/frontend-vite.zip
|
||||||
|
offline-backend-20260505/bundle/skills/
|
||||||
|
offline-backend-20260505/bundle/start_backend.0509.sh
|
||||||
|
offline-backend-20260505/bundle/start_backend.sh
|
||||||
|
offline-backend-20260505/bundle/stop_backend.sh
|
||||||
|
|
||||||
|
# offline-backend-20260512 runtime artifacts (large files, local config, data)
|
||||||
|
offline-backend-20260512/.env.example
|
||||||
|
offline-backend-20260512/.env
|
||||||
|
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
|
||||||
|
offline-backend-20260512/SHA256SUMS
|
||||||
|
offline-backend-20260512/config.yaml.example
|
||||||
|
offline-backend-20260512/data/
|
||||||
|
offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
|
||||||
|
offline-backend-20260512/extensions_config.json
|
||||||
|
offline-backend-20260512/skills/
|
||||||
|
offline-backend-20260512/wheelhouse/
|
||||||
|
offline-backend-20260512/backend/.deer-flow/data/*.db
|
||||||
|
offline-backend-20260512/backend/.deer-flow/data/*.db-*
|
||||||
|
offline-backend-20260512/backend/.deer-flow/*
|
||||||
|
offline-backend-20260512/backend/.env
|
||||||
|
offline-backend-20260512/backend/.env.local
|
||||||
|
offline-backend-20260512/start_backend.sh
|
||||||
|
offline-backend-20260512/stop_backend.sh
|
||||||
|
offline-backend-20260512/check_backend.sh
|
||||||
|
|
||||||
|
hermes-agent-main/
|
||||||
|
|
||||||
|
|
||||||
|
skills/*
|
||||||
|
|
||||||
|
offline-backend-20260512/backend/.deer-flow/*
|
||||||
|
|
||||||
|
# Per-user skill_evolution overrides — runtime data written by the API
|
||||||
|
offline-backend-20260512/backend/skill_evolution.json
|
||||||
|
skills/
|
||||||
|
hermes-agent-main/
|
||||||
|
.gitignore
|
||||||
|
offline-backend-20260512/backend/tests/*
|
||||||
|
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
|
||||||
|
# Keep formal Word OOXML and embedded-font regression coverage versioned.
|
||||||
|
!offline-backend-20260512/backend/tests/test_word_export.py
|
||||||
|
# Keep the model configuration regression coverage versioned even though
|
||||||
|
# this deployment repository otherwise ignores newly added backend tests.
|
||||||
|
!offline-backend-20260512/backend/tests/test_model_management.py
|
||||||
|
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
|
||||||
|
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
|
||||||
|
# Keep fixed-question exact-match and streaming regression coverage versioned.
|
||||||
|
!offline-backend-20260512/backend/tests/test_fixed_questions.py
|
||||||
|
# Keep configurable es_query system-prompt routing regression coverage versioned.
|
||||||
|
!offline-backend-20260512/backend/tests/test_es_query_routing_prompt.py
|
||||||
|
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
|
||||||
|
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||||
|
# Deep-research reliability regression coverage.
|
||||||
|
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||||
|
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||||
|
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||||
|
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
|
||||||
|
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
|
||||||
|
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
|
||||||
|
deploy
|
||||||
|
.history
|
||||||
|
|
||||||
|
# Cloned third-party repos (local only)
|
||||||
|
git-clone/
|
||||||
|
|
||||||
|
# Repo-root offline dependency pack (local only)
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Frontend offline pack working directories
|
||||||
|
frontend-web/.offline-pack-build/
|
||||||
|
frontend-web/.offline-extract-test/
|
||||||
|
frontend-web-node_modules-offline-*.tar.gz
|
||||||
|
|
||||||
|
# Local dependency caches and backend scratch archives
|
||||||
|
/.pnpm-store/
|
||||||
|
/offline-backend-20260512/backend/backend*.zip
|
||||||
|
|
||||||
|
# Local offline bundles and scratch tooling
|
||||||
|
/*.tar.gz
|
||||||
|
/offline-backend-20260512/*.zip
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
.codegraph
|
||||||
|
codegraph.mdc
|
||||||
|
|
||||||
|
docs/aidev-docs
|
||||||
|
docs/backups
|
||||||
|
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||||
|
!frontend-web/src/components/workspace/skills/
|
||||||
|
|
||||||
|
# uv lockfile (not tracked per project preference)
|
||||||
|
offline-backend-20260512/backend/uv.lock
|
||||||
|
|
||||||
|
# Local-only scratch directory (not committed)
|
||||||
|
0622/
|
||||||
|
|
||||||
|
# Python bytecode cache
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
@@ -80,8 +80,6 @@ The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph
|
|||||||
|
|
||||||
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
||||||
|
|
||||||
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `AGENTS.md`/`CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
|
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||||
@@ -102,7 +100,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
|||||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
|
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
|
||||||
- **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ 每次检索 JSON 原样落盘 `/mnt/user-data/workspace/hits/qN.json` → **一条命令** `task-report-build`(group `all`)技能函数转换成 14 类 `task-reports.json`(模型禁止手写该 JSON;分页技能 `task-report-{enemy,our,env,judge}` 仅用于事后补跑单页;五者都是共享引擎 `task-report-lib/convert_lib.py` 的瘦 CLI,`resolve_hit_files` 对虚拟绝对路径读空的情况自动回退到相对写法并在 `warnings` 里列出尝试过的路径,免得智能体自己写脚本排查)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` → 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "Line truncated
|
- **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` + `task-reports.json`(14 个 categoryType 的 `{contentJson, categoryType}` 数组,contentJson 为 JSON 字符串)→ 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "task-report:saved", taskId: String(taskId)}, "*")`(`notifyTaskReportSaved`,`agentfx-report-schema.ts`)——卡片路径在 `TaskReportImportCard` 导入成功回调里直发;技能路径由 `AgentChatPage` 从消息流检测 `task_report_import.py` 的成功 stdout(`"success": true` + `task_id`)代发——**仅在回合结束后检测**(`!thread.isLoading` 才扫描;流式过程中 messages Line truncated
|
||||||
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
|
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph
|
|||||||
|
|
||||||
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
||||||
|
|
||||||
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
|
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ const LEGACY_LOCAL_API_BASE_URL = "http://localhost:8001";
|
|||||||
const LEGACY_LOCAL_WEB_BASE_URL = "http://localhost:5174";
|
const LEGACY_LOCAL_WEB_BASE_URL = "http://localhost:5174";
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
let isPolling = false;
|
let isPolling = false;
|
||||||
let startupPromise = null;
|
|
||||||
|
|
||||||
const POLLING_SCHEDULE_SETTING_KEYS = new Set(["taskPollingEnabled", "taskPollIntervalSeconds"]);
|
|
||||||
|
|
||||||
function delay(ms) {
|
function delay(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -15,14 +12,6 @@ function delay(ms) {
|
|||||||
|
|
||||||
async function getSettings() {
|
async function getSettings() {
|
||||||
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
||||||
const discovered = await chrome.storage.local.get({
|
|
||||||
discoveredApiBaseUrl: "",
|
|
||||||
discoveredWebBaseUrl: "",
|
|
||||||
});
|
|
||||||
if (settings.autoDiscoverServiceUrl) {
|
|
||||||
settings.apiBaseUrl = discovered.discoveredApiBaseUrl || settings.apiBaseUrl;
|
|
||||||
settings.webBaseUrl = discovered.discoveredWebBaseUrl || settings.webBaseUrl;
|
|
||||||
}
|
|
||||||
const updates = {};
|
const updates = {};
|
||||||
if (!settings.apiBaseUrl || settings.apiBaseUrl === LEGACY_LOCAL_API_BASE_URL) {
|
if (!settings.apiBaseUrl || settings.apiBaseUrl === LEGACY_LOCAL_API_BASE_URL) {
|
||||||
updates.apiBaseUrl = DEER_CLIPPER_DEFAULTS.apiBaseUrl;
|
updates.apiBaseUrl = DEER_CLIPPER_DEFAULTS.apiBaseUrl;
|
||||||
@@ -80,7 +69,10 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
add(currentApiBase);
|
||||||
add(webBase);
|
add(webBase);
|
||||||
|
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
|
||||||
|
add(LEGACY_LOCAL_API_BASE_URL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(normalizeServiceBase(webBase));
|
const url = new URL(normalizeServiceBase(webBase));
|
||||||
@@ -93,10 +85,6 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
|
|||||||
// Ignore invalid candidates.
|
// Ignore invalid candidates.
|
||||||
}
|
}
|
||||||
|
|
||||||
add(currentApiBase);
|
|
||||||
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
|
|
||||||
add(LEGACY_LOCAL_API_BASE_URL);
|
|
||||||
|
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +93,6 @@ async function browserContextStatus(apiBaseUrl) {
|
|||||||
const response = await fetch(`${apiBaseUrl}/api/browser-context/status`, {
|
const response = await fetch(`${apiBaseUrl}/api/browser-context/status`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
// The gateway may enforce authentication globally before this otherwise
|
|
||||||
// public discovery endpoint is reached. A 401/403 still proves that this
|
|
||||||
// candidate is the zncm API paired with the open frontend tab.
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
|
||||||
return { enabled: true, authenticationRequired: true };
|
|
||||||
}
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -219,12 +201,9 @@ async function discoverServiceBases(settings, force = false) {
|
|||||||
settings.webBaseUrl !== next.webBaseUrl ||
|
settings.webBaseUrl !== next.webBaseUrl ||
|
||||||
settings.lastServiceSyncSource !== next.lastServiceSyncSource
|
settings.lastServiceSyncSource !== next.lastServiceSyncSource
|
||||||
) {
|
) {
|
||||||
// Discovered endpoints are runtime state, not user-authored preferences.
|
await chrome.storage.sync.set({
|
||||||
// Keep them in storage.local so Chrome sync write quotas cannot break task
|
apiBaseUrl: next.apiBaseUrl,
|
||||||
// polling. The options page may still persist explicit user choices.
|
webBaseUrl: next.webBaseUrl,
|
||||||
await chrome.storage.local.set({
|
|
||||||
discoveredApiBaseUrl: next.apiBaseUrl,
|
|
||||||
discoveredWebBaseUrl: next.webBaseUrl,
|
|
||||||
lastServiceSyncAt: next.lastServiceSyncAt,
|
lastServiceSyncAt: next.lastServiceSyncAt,
|
||||||
lastServiceSyncSource: next.lastServiceSyncSource,
|
lastServiceSyncSource: next.lastServiceSyncSource,
|
||||||
});
|
});
|
||||||
@@ -847,50 +826,19 @@ async function pollOnce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializePolling() {
|
async function startup() {
|
||||||
let settings = await getSettings();
|
let settings = await getSettings();
|
||||||
try {
|
settings = await discoverServiceBases(settings);
|
||||||
settings = await discoverServiceBases(settings);
|
settings = await syncAuthToken(settings);
|
||||||
} catch (error) {
|
|
||||||
// Service discovery persists diagnostic metadata in storage.sync. Chrome can
|
|
||||||
// temporarily reject those writes after its quota is reached; polling must
|
|
||||||
// still start with the last known settings.
|
|
||||||
console.warn("zncm service discovery failed during startup", error);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
settings = await syncAuthToken(settings);
|
|
||||||
} catch (error) {
|
|
||||||
// A failed status/token write must not prevent alarms from being installed.
|
|
||||||
// pollOnce() will retry token synchronization before claiming each task.
|
|
||||||
console.warn("zncm token sync failed during startup", error);
|
|
||||||
}
|
|
||||||
await refreshAlarm(settings);
|
await refreshAlarm(settings);
|
||||||
await scheduleNextPoll(1000);
|
await scheduleNextPoll(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startup() {
|
chrome.runtime.onInstalled.addListener(() => void startup());
|
||||||
if (!startupPromise) {
|
chrome.runtime.onStartup.addListener(() => void startup());
|
||||||
startupPromise = initializePolling().finally(() => {
|
|
||||||
startupPromise = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return startupPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function restartPollingSchedule() {
|
|
||||||
const settings = await getSettings();
|
|
||||||
await refreshAlarm(settings);
|
|
||||||
await scheduleNextPoll(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
|
|
||||||
chrome.runtime.onStartup.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
|
|
||||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (
|
if (areaName === "sync") {
|
||||||
areaName === "sync" &&
|
void startup();
|
||||||
Object.keys(changes || {}).some((key) => POLLING_SCHEDULE_SETTING_KEYS.has(key))
|
|
||||||
) {
|
|
||||||
void restartPollingSchedule().catch((error) => console.warn("zncm polling reschedule failed", error));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
@@ -954,4 +902,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
void startup().catch((error) => console.warn("zncm startup failed", error));
|
void startup();
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
const assert = require("node:assert/strict");
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
const test = require("node:test");
|
|
||||||
const vm = require("node:vm");
|
|
||||||
|
|
||||||
const BACKGROUND_PATH = path.join(__dirname, "background.js");
|
|
||||||
|
|
||||||
function createHarness() {
|
|
||||||
let storageChangedListener;
|
|
||||||
let alarmCreateCount = 0;
|
|
||||||
const settings = {
|
|
||||||
apiBaseUrl: "http://localhost:8001",
|
|
||||||
webBaseUrl: "http://localhost:5174",
|
|
||||||
autoDiscoverServiceUrl: false,
|
|
||||||
accessToken: "",
|
|
||||||
tokenSyncEnabled: false,
|
|
||||||
taskPollingEnabled: true,
|
|
||||||
workerId: "test-worker",
|
|
||||||
taskPollIntervalSeconds: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
const chrome = {
|
|
||||||
alarms: {
|
|
||||||
clear: async () => {},
|
|
||||||
create: async () => {
|
|
||||||
alarmCreateCount += 1;
|
|
||||||
},
|
|
||||||
onAlarm: { addListener: () => {} },
|
|
||||||
},
|
|
||||||
cookies: { get: async () => null },
|
|
||||||
runtime: {
|
|
||||||
onInstalled: { addListener: () => {} },
|
|
||||||
onMessage: { addListener: () => {} },
|
|
||||||
onStartup: { addListener: () => {} },
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
local: {
|
|
||||||
get: async (defaults) => ({ ...defaults }),
|
|
||||||
set: async () => {},
|
|
||||||
},
|
|
||||||
session: { get: async () => ({ actSessions: {} }), set: async () => {} },
|
|
||||||
sync: {
|
|
||||||
get: async () => ({ ...settings }),
|
|
||||||
set: async (updates) => Object.assign(settings, updates),
|
|
||||||
},
|
|
||||||
onChanged: {
|
|
||||||
addListener: (listener) => {
|
|
||||||
storageChangedListener = listener;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tabs: { query: async () => [] },
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
chrome,
|
|
||||||
console,
|
|
||||||
fetch: async () => {
|
|
||||||
throw new Error("offline test");
|
|
||||||
},
|
|
||||||
importScripts: () => {},
|
|
||||||
setTimeout: () => 1,
|
|
||||||
clearTimeout: () => {},
|
|
||||||
URL,
|
|
||||||
DEER_CLIPPER_DEFAULTS: settings,
|
|
||||||
deerNormalizeBaseUrl: (value) => String(value || "").replace(/\/+$/, ""),
|
|
||||||
deerMakeWorkerId: () => "test-worker",
|
|
||||||
deerAuthHeaders: () => ({}),
|
|
||||||
deerExtractAuthTokenFromPage: () => ({ token: "" }),
|
|
||||||
};
|
|
||||||
context.globalThis = context;
|
|
||||||
vm.runInNewContext(fs.readFileSync(BACKGROUND_PATH, "utf8"), context, {
|
|
||||||
filename: BACKGROUND_PATH,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
emitStorageChange(changes) {
|
|
||||||
storageChangedListener(changes, "sync");
|
|
||||||
},
|
|
||||||
getAlarmCreateCount: () => alarmCreateCount,
|
|
||||||
resetAlarmCreateCount: () => {
|
|
||||||
alarmCreateCount = 0;
|
|
||||||
},
|
|
||||||
getApiCandidates: (webBase, currentApiBase) =>
|
|
||||||
vm.runInContext(`apiCandidatesForWebBase(${JSON.stringify(webBase)}, ${JSON.stringify(currentApiBase)})`, context),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
|
||||||
|
|
||||||
test("self-written sync metadata does not restart browser task polling", async () => {
|
|
||||||
const harness = createHarness();
|
|
||||||
await settle();
|
|
||||||
harness.resetAlarmCreateCount();
|
|
||||||
|
|
||||||
harness.emitStorageChange({
|
|
||||||
lastTokenSyncAt: { oldValue: "before", newValue: "after" },
|
|
||||||
});
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
assert.equal(harness.getAlarmCreateCount(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("polling configuration changes restart browser task polling", async () => {
|
|
||||||
const harness = createHarness();
|
|
||||||
await settle();
|
|
||||||
harness.resetAlarmCreateCount();
|
|
||||||
|
|
||||||
harness.emitStorageChange({
|
|
||||||
taskPollingEnabled: { oldValue: false, newValue: true },
|
|
||||||
});
|
|
||||||
await settle();
|
|
||||||
|
|
||||||
assert.equal(harness.getAlarmCreateCount(), 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("service discovery prefers the API paired with the open zncm tab", async () => {
|
|
||||||
const harness = createHarness();
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
Array.from(harness.getApiCandidates("http://localhost:5174", "http://47.94.209.59:2026")),
|
|
||||||
[
|
|
||||||
"http://localhost:5174",
|
|
||||||
"http://localhost:8001",
|
|
||||||
"http://47.94.209.59:2026",
|
|
||||||
"http://localhost:8001",
|
|
||||||
].filter((value, index, values) => values.indexOf(value) === index),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -7,8 +7,6 @@
|
|||||||
const HOST_ID = "zncm-page-assistant-host";
|
const HOST_ID = "zncm-page-assistant-host";
|
||||||
const state = {
|
const state = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
taskPollingEnabled: true,
|
|
||||||
webBaseUrl: "",
|
|
||||||
open: false,
|
open: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
threadId: "",
|
threadId: "",
|
||||||
@@ -711,58 +709,20 @@
|
|||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
||||||
state.enabled = Boolean(settings.pageAssistantEnabled);
|
state.enabled = Boolean(settings.pageAssistantEnabled);
|
||||||
state.taskPollingEnabled = Boolean(settings.taskPollingEnabled);
|
|
||||||
state.webBaseUrl = deerNormalizeBaseUrl(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
|
|
||||||
ensureWidget();
|
ensureWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (areaName !== "sync") {
|
if (areaName !== "sync" || !changes.pageAssistantEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changes.pageAssistantEnabled) {
|
state.enabled = Boolean(changes.pageAssistantEnabled.newValue);
|
||||||
state.enabled = Boolean(changes.pageAssistantEnabled.newValue);
|
ensureWidget();
|
||||||
ensureWidget();
|
if (state.enabled) {
|
||||||
if (state.enabled) {
|
setStatus("页面问答框已开启。");
|
||||||
setStatus("页面问答框已开启。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changes.taskPollingEnabled) {
|
|
||||||
state.taskPollingEnabled = Boolean(changes.taskPollingEnabled.newValue);
|
|
||||||
}
|
|
||||||
if (changes.webBaseUrl) {
|
|
||||||
state.webBaseUrl = deerNormalizeBaseUrl(changes.webBaseUrl.newValue || DEER_CLIPPER_DEFAULTS.webBaseUrl);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function isZncmPage() {
|
|
||||||
const appPath = `${location.pathname}${location.hash}`.toLowerCase();
|
|
||||||
if (
|
|
||||||
appPath.includes("/page/workspace") ||
|
|
||||||
appPath.includes("/chats/") ||
|
|
||||||
appPath.includes("#/login")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const configured = new URL(state.webBaseUrl);
|
|
||||||
return configured.origin === location.origin;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MV3 service workers may be suspended between their short polling timers,
|
|
||||||
// while chrome.alarms cannot reliably run more often than every 30 seconds.
|
|
||||||
// A visible zncm tab provides a cheap wake-up signal so browser_act requests
|
|
||||||
// are normally claimed within the backend/model timeout window.
|
|
||||||
setInterval(() => {
|
|
||||||
if (!state.taskPollingEnabled || !isZncmPage()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.runtime.sendMessage({ type: "zncm-poll-now" }).catch(() => {});
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!state.enabled || !ui.pageTitle) {
|
if (!state.enabled || !ui.pageTitle) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
# Wiki 数据流转与本地编码验收(2026-09-03)
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
本地 BGE-M3 小批量编码已安装、配置并实际运行。已有 Wiki 的向量化、自动沉淀、
|
|
||||||
普通库文件导出、助手库异步导入、助手库文件导出、空普通库回流及两端向量检索已实测通过。
|
|
||||||
**技能自动生成 Wiki 尚未完成验收**:WeKnora 的两个生成模型都返回 HTTP 402
|
|
||||||
`Insufficient Balance`。不能把后半段通过等同于全链路通过。
|
|
||||||
|
|
||||||
本次修改运行在本地 DeerFlow 开发服务(前端 5175、后端 8001),连接用户提供的远端
|
|
||||||
WeKnora;没有把业务代码发布到服务器,没有提交 Git,也没有直接写 WeKnora 图数据库。
|
|
||||||
|
|
||||||
## 本地编码配置
|
|
||||||
|
|
||||||
- 实现:FastEmbed / ONNX Runtime CPU,官方 `BAAI/bge-m3` 权重。
|
|
||||||
- 配置名:`bge-embedding-m3`,1024 维,每批 2 条,2 个 CPU 推理线程。
|
|
||||||
- 运行依赖:FastEmbed 随标准后端依赖安装;模型必须提前放在离线目录中,仍按需加载,不影响主服务启动。
|
|
||||||
- 权重:约 2.3 GB,持久缓存 `C:\Users\ji\.cache\deerflow\embeddings`,不进入业务仓库。
|
|
||||||
- 配置:`llmwiki.local_wiki_index.embedding.provider: local`;可用
|
|
||||||
`local_model_path` 指定离线权重目录,详见后端 README。
|
|
||||||
- 查询向量在本地实时编码。已有 Wiki 在内容哈希、模型指纹匹配且向量非空时直接复用。
|
|
||||||
不同模型或维度不能混用;导入不静默重新编码。
|
|
||||||
- 原始文档分段向量与模型生成后的 Wiki 文本不同,不能相互冒用。本次新建的是 Wiki
|
|
||||||
正文的 BGE-M3 索引,不是把既有 512 维文档向量改名为 1024 维。
|
|
||||||
|
|
||||||
## 实际数据验收
|
|
||||||
|
|
||||||
来源是已有普通知识库“图片测试111”,没有改写其 Wiki 内容。
|
|
||||||
|
|
||||||
| 阶段 | Wiki | Wiki 向量 | 实体 | 引用关系 |
|
|
||||||
| --- | ---: | ---: | ---: | ---: |
|
|
||||||
| 普通知识库导出 | 19 | 101 | 19 | 108 |
|
|
||||||
| 助手知识库导入后导出 | 19 | 101 | 19 | 108 |
|
|
||||||
| 新建普通知识库回流后导出 | 19 | 101 | 19 | 108 |
|
|
||||||
| 知识梳理总库导出(7 个来源记录) | 19 | 101 | 19 | 108 |
|
|
||||||
|
|
||||||
逐项断言通过:
|
|
||||||
|
|
||||||
1. 三份包的 Wiki slug、标题、正文、摘要、页面类型、别名、分类路径、出链一致。
|
|
||||||
2. 101 条向量的原始 float32 字节、分段编号、维度和模型指纹完全一致。
|
|
||||||
3. 将导入前后实体 ID 映射回原 Wiki slug 后,108 条关系的端点及谓词集合完全一致。
|
|
||||||
4. 总库导出没有因多个来源重复同步而重复导出同一当前页面的分段向量。
|
|
||||||
5. 元数据回归测试另覆盖 tags、目录恢复、原始文档片段不计入 Wiki 数量、原生技能无
|
|
||||||
ownership 记录时可被选中、短 Wiki 不漏向量、模型指纹不兼容时不假装向量检索。
|
|
||||||
|
|
||||||
普通包约 1.01 MB,助手包约 0.79 MB,回流包约 0.83 MB。普通包还带 2 个原始文档及
|
|
||||||
108 个原始片段;助手导出提供 Wiki、Wiki 向量和关系,不重新上传原始文档触发二次生成。
|
|
||||||
|
|
||||||
### 检索
|
|
||||||
|
|
||||||
实际查询:“军方用什么工具评估极端天气带来的风险?”
|
|
||||||
|
|
||||||
- 回流普通库 `/api/llmwiki/search` 返回 `mode=vector`,首条“国防气候评估工具”。
|
|
||||||
- 助手库 `/api/assistant-knowledge/search` 返回 `retrieval_mode=vector`,首条同上;
|
|
||||||
首个命中分段余弦相似度约 0.6343。
|
|
||||||
- 浏览器实际提交总库检索,显示“向量检索 · 余弦相似度”和相同命中;不是只检查返回字段。
|
|
||||||
- 普通库采用已有页面级聚合评分,因此页面最终分数与单分段余弦值不必相等。
|
|
||||||
- 回流的向量写入 **DeerFlow 管理的本地 Wiki 索引**。未写入 WeKnora 原生向量数据库,
|
|
||||||
本次结论适用于本系统问答检索,不能声称原生 WeKnora 自身的检索也已完成该向量导入。
|
|
||||||
|
|
||||||
### 页面
|
|
||||||
|
|
||||||
普通与助手库复用同一 Wiki 阅读组件:目录树、类型/标签筛选、分类面包屑、别名标签、
|
|
||||||
正文内链、出链、反向链接及邻接关系视图。窄窗口可收起目录。
|
|
||||||
|
|
||||||
浏览器验证了“索引 → 国防气候评估工具”、展开关系视图、助手 Wiki 跳回普通库来源,
|
|
||||||
以及普通 Wiki 的“在知识梳理中查看”反向跳转。
|
|
||||||
|
|
||||||
### 验收数据位置
|
|
||||||
|
|
||||||
- 原有普通库:`48f669dc-433e-4aab-b5ea-897b6f7a3d2f`
|
|
||||||
- 知识梳理总库:`c3fd9841-0718-44dc-860e-aae3020fd485`
|
|
||||||
- 最终助手测试库:`489be905-08c8-40fe-8d60-42ac21dd945a`
|
|
||||||
- 最终普通回流测试库:`428162aa-03f6-4c61-8994-360f2f39e0c9`
|
|
||||||
|
|
||||||
新建测试库名称均以“Wiki链路验收-0903-1826”开头,保留用于复核;未删除用户已有库。
|
|
||||||
本地 `.runtime/wiki-live-result.json` 保存验收结果,`.runtime/wiki_live_acceptance.py`
|
|
||||||
保存本次验收流程,均为忽略的运行产物,不包含认证口令。
|
|
||||||
|
|
||||||
## 这次实际修复的问题
|
|
||||||
|
|
||||||
- 内置技能只有磁盘目录、没有归属记录时,被错误判定为不存在。
|
|
||||||
- 技能应上传去代码后的知识正文,让 WeKnora 生成 Wiki,而不是伪造一个 `skill` 类型页面。
|
|
||||||
- Wiki 生成等待使用实际 stats 接口;零页结果明确失败,不无期限等待。
|
|
||||||
- 向量化完成后自动导出沉淀到总库,并阻断导出回调重复触发全量同步。
|
|
||||||
- 短摘要和短正文同时存在时,旧分段阈值会将整页过滤为零向量。
|
|
||||||
- 分类路径必须先创建 WeKnora folder 并绑定 folder_id;只传 category_path 会被服务端清空。
|
|
||||||
- Wiki 的标签/分类/别名/原始引用保留;图节点绑定真实 Wiki,避免生成重复的空实体页面。
|
|
||||||
- 保留归一化 float32 原始字节;导入中不能用 `section_index or index`,因为合法编号 0 会被改写。
|
|
||||||
- 总库多个来源保留审计,但检索与导出只使用当前 Wiki 版本对应的向量,避免旧贡献污染检索。
|
|
||||||
- 包解析改为同盘临时 SQLite,上传先落盘再异步导入,导出完成后才提供文件下载。
|
|
||||||
|
|
||||||
## 阻塞与未通过的范围
|
|
||||||
|
|
||||||
1. 技能 `knowledge-base-ingest` 已送入专门创建的普通测试库,但远端
|
|
||||||
`deepseek-v4-flash` 和 `qwen3.6-flash` 均因余额不足失败。WeKnora 即使失败也可能把
|
|
||||||
文档 parse_status 标为 completed。需恢复可用生成模型后重新运行技能全链路验收。
|
|
||||||
2. 已采用磁盘暂存和有界批处理,但本次只有约 1 MB 实测,**未做数 GB 压测、断点续传、
|
|
||||||
进程中断恢复验收**。大包部署仍需磁盘容量、代理 body size 和超时配置。
|
|
||||||
3. 当前实体去重主要按类型及规范化名称;这次没有完成跨别名、歧义实体的模型对齐及
|
|
||||||
多来源事实冲突合并验收,不能视为这一历史需求已全面完成。
|
|
||||||
4. 重复上传仍会保留独立来源记录。向量导出已消除旧版本重复,但上传身份幂等、部分远端
|
|
||||||
写入失败后的无损重试、跨实例图片资源打包仍需独立验证。
|
|
||||||
5. 当前版本已补充自动增量链路:服务启动立即补扫,之后默认每 30 秒扫描全部普通
|
|
||||||
知识库;Wiki 抽取完成后无需手工点击向量化,并自动以来源快照覆盖同步到知识梳理总库。
|
|
||||||
6. 回流当前只允许新建空 Wiki 普通库,避免覆盖已有页面。普通库现有页面列表的超大规模
|
|
||||||
分页与助手问答的最终生成回答不属于这轮通过结论。
|
|
||||||
|
|
||||||
## 自动化检查
|
|
||||||
|
|
||||||
- 后端相关测试:118 passed(assistant repository、Wiki sync、package roundtrip、
|
|
||||||
WeKnora 接口、向量检索与知识库管理)。
|
|
||||||
- 修改的 Python 功能文件 Ruff 检查通过;app.py 既有启动顺序导致的 E402 不在此次改动中处理。
|
|
||||||
- `git diff --check` 通过(只有仓库换行提示)。
|
|
||||||
- 前端完整 `pnpm typecheck` 未通过:57 个既有/本轮范围外错误,涉及 nextra 缺失、Markdown
|
|
||||||
插件类型及其他模块;本轮新建/修改的 Wiki 组件、助手 API、技能面板没有出现在错误清单中。
|
|
||||||
浏览器实际页面与跳转已验证,但不能声称全项目构建通过。
|
|
||||||
@@ -1,719 +0,0 @@
|
|||||||
# AgentScope 2.0 多智能体报告协作工作台——前端实施计划
|
|
||||||
|
|
||||||
> 文档状态:前端代码已落地、契约冻结;联调等待后端运行中干预与报告改写
|
|
||||||
> 编写日期:2026-09-02;进度更新日期:2026-09-06
|
|
||||||
> 目标项目:`F:\react01\deeflow-code\deerflow-server\frontend-web`
|
|
||||||
> 视觉与交互参考:`F:\react01\coze-frontend\frontend\apps\coze-studio`
|
|
||||||
> 参考页面:`http://localhost:3000/work_flow?space_id=1&workflow_id=wf_88cb9f4c6642`
|
|
||||||
> 配套后端计划:`docs/agentscope-多智能体报告协作工作台-后端实施计划.md`
|
|
||||||
> 本文只规划新增页面,不修改现有多智能体会商页面,不复用旧 Workflow Studio 执行状态。
|
|
||||||
|
|
||||||
## 0. 当前进度
|
|
||||||
|
|
||||||
`frontend-web/src/report-collaboration/` 已实现两栏工作台、会话/方案/run API 客户端、mock driver、SSE reducer、`MessageList` 适配、方案卡、画布、节点抽屉、Composer、报告预览与历史。`RC-FE-001`~`013` 的页面骨架与契约层视为完成。
|
|
||||||
|
|
||||||
后端已通规划前路径(澄清 → 出方案 → 选定 → `queued` run),见后端计划 §0。前端真实执行体验仍受阻:
|
|
||||||
|
|
||||||
- `GET /runs/{id}/stream` 已挂载(`RC-BE-011`);dispatcher/租约已落地(`RC-BE-012`),默认 `worker_enabled=false` 仍不自动领取 queued run。
|
|
||||||
- 报告改写 / 恢复接口已由后端 `RC-BE-015` 挂载。
|
|
||||||
- 质量角色已由 `QualityRoleKernel` 跑通(`RC-BE-013`);真实 AgentScope 模型/`web_search` 仍待后续票。
|
|
||||||
|
|
||||||
联调顺序:先打开 `report_collaboration.enabled` 验证规划前 REST 与 `GET /runs/{id}/stream` 重放;画布自动执行需同时打开 `worker_enabled`(默认关)。
|
|
||||||
|
|
||||||
## 1. 前端目标
|
|
||||||
|
|
||||||
新增独立的「报告协作」工作台。用户可以通过对话提出任意报告需求,查看系统生成的多个协作方案,点击方案卡片在流程图中预览协作结构,确认后观察多智能体执行,并在全过程通过统一对话框提出修改意见。
|
|
||||||
|
|
||||||
前端必须满足:
|
|
||||||
|
|
||||||
1. 页面采用“左侧对话、右侧流程画布”的固定两栏布局,画布、工具栏、弹窗和抽屉参考 Coze Studio 页面风格。
|
|
||||||
2. 消息列表必须复用 DeerFlow 现有 `MessageList`,不复制 Coze 的会话消息实现。
|
|
||||||
3. 所有执行过程、AgentScope 团队消息、智能体回复、工具调用及其原始返回、错误、协助卡、返工和最终报告入口都通过现有消息协议按时间顺序显示在左侧对话区,不设置独立执行结果面板,也不另做“阶段摘要”替代实时消息。
|
|
||||||
4. 候选方案、流程节点、对话进度和报告使用同一份后端事实,不各自推断状态。
|
|
||||||
5. 用户可在规划中、运行中、等待输入和报告完成后继续对话。
|
|
||||||
6. 用户协助卡的出现和提交都不能在前端被解释为“节点已经完成”。
|
|
||||||
7. 页面刷新、SSE 断线重连、历史会话切换后能恢复到一致状态。
|
|
||||||
|
|
||||||
暂定:
|
|
||||||
|
|
||||||
- 页面名称:`报告协作`。
|
|
||||||
- 路由:`/page/workspace/report-collaboration`。
|
|
||||||
- 会话路由:`/page/workspace/report-collaboration/:sessionId`。
|
|
||||||
- 默认候选方案数量:3。
|
|
||||||
- 点击画布节点时打开覆盖式节点说明抽屉;抽屉只展示节点职责和合同,不展示节点执行结果。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 范围与非目标
|
|
||||||
|
|
||||||
### 2.1 本期范围
|
|
||||||
|
|
||||||
- 新建、查看、重命名、删除协作报告会话。
|
|
||||||
- 使用对话澄清报告需求。
|
|
||||||
- 显示 2~3 个候选方案卡片。
|
|
||||||
- 卡片与右侧流程图预览联动。
|
|
||||||
- 节点说明抽屉:职责、所依据的信息类型、预期产出、工具、资料要求和验收条件。
|
|
||||||
- 实时展示节点排队、运行、校验、等待用户、完成、返工、失败和取消。
|
|
||||||
- 在左侧对话时间线中直接展示每个智能体的流式文本、团队对话、工具调用和工具返回,并展示审稿意见、报告文件和报告版本。
|
|
||||||
- 并行阶段用横向的智能体选择器切换当前查看对象;选择器只控制同一份消息流的聚焦范围,不创建另一套执行结果视图。
|
|
||||||
- 运行期间发送补充要求、重新检索、重新分析、节点重跑等指令。
|
|
||||||
- 完成后进行报告问答、章节重写、全文润色和版本恢复。
|
|
||||||
- 报告模板选择、Markdown 预览、Markdown/Word 下载。
|
|
||||||
- 响应式布局、键盘操作、加载反馈和错误恢复。
|
|
||||||
|
|
||||||
### 2.2 明确不做
|
|
||||||
|
|
||||||
- 不引入 `@coze-workflow/playground` 或 `@coze-arch/coze-design`。
|
|
||||||
- 不加载 Coze workflow definition、run 或 node run。
|
|
||||||
- 不允许前端拖拽连线后直接改变执行逻辑。
|
|
||||||
- 不新增第二套消息气泡、Markdown、引用和文件卡渲染器。
|
|
||||||
- 不展示模型原始 chain-of-thought、系统/内部 prompt、密钥和被标记为内部控制信息的字段。
|
|
||||||
- 不把用户有权查看的 AgentScope TeamSay、智能体文本、工具调用或工具返回压缩成“正在检索”等摘要卡;这些原始业务消息必须进入复用的 `MessageList`。敏感字段只能按统一脱敏策略替换,不能以摘要替代。
|
|
||||||
- 不增加第三栏、常驻详情栏、节点执行结果栏或独立执行时间线。
|
|
||||||
- 节点抽屉不展示节点生成正文、工具返回、证据列表、错误堆栈或 attempt 历史;这些过程统一在左侧 `MessageList` 的原生消息/工具步骤中查看。
|
|
||||||
- 不改造现有圆桌、多智能体会商、深度研究和 AI 写作页面。
|
|
||||||
- 不把浏览器状态作为业务事实源。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 参考页面行为映射
|
|
||||||
|
|
||||||
| 目标行为 | Coze 参考文件 | DeerFlow 目标组件 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 两栏骨架 | `workflow-studio-layout.tsx` 的面板比例与分隔条 | `ReportCollaborationLayout.tsx` | 左对话、右画布;只保留一条可调整分隔线 |
|
|
||||||
| 顶栏 | `workflow-studio-header.tsx` | `ReportCollaborationHeader.tsx` | 替换为会话、模板、报告、停止等操作 |
|
|
||||||
| 候选卡片 | `workflow-proposal-cards.tsx` | `ReportPlanCards.tsx` | 点击预览,明确确认后才执行 |
|
|
||||||
| 右侧画布 | `workflow-canvas.tsx` | `ReportFlowCanvas.tsx` | 使用 `@xyflow/react` 重建,不引入 Coze Playground |
|
|
||||||
| 左侧消息区 | `run-conversation-panel.tsx` | `ReportConversationPanel.tsx` | 外层风格参考 Coze,内部复用 DeerFlow `MessageList` |
|
|
||||||
| 节点说明抽屉 | Coze 节点配置面板的视觉与抽屉行为 | `NodeInspectorDrawer.tsx` | 点击画布节点打开,只展示职责与输入输出合同 |
|
|
||||||
| 全程执行反馈 | 参考其进度和状态表达 | `AgentStageSelector` + DeerFlow `MessageList` | 选择智能体后直接显示其流式消息、TeamSay、工具调用与返回;不再合成为阶段摘要 |
|
|
||||||
| 状态栏 | `connection-status.tsx` | `ConnectionStatusBar.tsx` | 展示连接、重连和事件游标 |
|
|
||||||
| 对话历史 | `left-panel-tabs.tsx` | `ConversationSidebarTabs.tsx` | 对话与历史,不增加“节点资源”Tab |
|
|
||||||
| 异步反馈 | 规划进度、运行状态 | MessageList 步骤条 + 画布状态 | 操作超过 300ms 必须有反馈 |
|
|
||||||
|
|
||||||
主要参考源:
|
|
||||||
|
|
||||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-layout.tsx`
|
|
||||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-composition.tsx`
|
|
||||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-header.tsx`
|
|
||||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\run-conversation\workflow-proposal-cards.tsx`
|
|
||||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\styles.module.less`
|
|
||||||
|
|
||||||
若直接复制 Apache-2.0 源码片段或样式,必须保留版权头并维护来源说明;默认做行为映射和视觉重建。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 页面结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
┌──────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 返回 报告协作 / 会话标题 保存状态 模板 报告 停止/继续 更多 │
|
|
||||||
├────────────────────────────────┬─────────────────────────────────────────┤
|
|
||||||
│ 对话 / 历史 │ 协作流程图 │
|
|
||||||
│ │ │
|
|
||||||
│ DeerFlow MessageList │ @xyflow/react │
|
|
||||||
│ · 并行时:[协调者][市场][政策] │ · 节点状态与活动连线 │
|
|
||||||
│ · 用户与协调者/成员消息 │ · 缩放、适配视图、小地图 │
|
|
||||||
│ · 原生 ToolCall / ToolMessage │ · 点击节点打开覆盖式说明抽屉 │
|
|
||||||
│ · 协助卡、报告文件和版本 │ │
|
|
||||||
│ │ │
|
|
||||||
│ 统一输入框 │ │
|
|
||||||
├────────────────────────────────┴─────────────────────────────────────────┤
|
|
||||||
│ 已连接 · run_xxx · 事件 128 │
|
|
||||||
└──────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
节点抽屉覆盖在右侧画布之上,不占用第三栏:
|
|
||||||
|
|
||||||
```text
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ 节点名称 关闭 × │
|
|
||||||
│ 角色 / 当前状态 │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ 节点职责 │
|
|
||||||
│ 要根据什么:所需输入、上游类型、资料要求 │
|
|
||||||
│ 要产出什么:成果类型、格式、验收条件 │
|
|
||||||
│ 可用工具与边界 │
|
|
||||||
│ 上下游关系 │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ 在对话中对此节点提出意见 │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
抽屉明确不展示实际执行结果、生成正文、工具返回、来源明细、错误堆栈或 attempt 时间线;这些信息只在左侧原生消息流中展示。
|
|
||||||
|
|
||||||
布局默认值:
|
|
||||||
|
|
||||||
- 顶栏高度:56px。
|
|
||||||
- 左侧面板:默认 480px,最小 360px,最大 720px。
|
|
||||||
- 右侧画布:最小 480px,使用剩余空间。
|
|
||||||
- 节点说明抽屉:覆盖画布右侧,建议宽度 420px;关闭后完整归还画布空间。
|
|
||||||
- 状态栏:24~28px。
|
|
||||||
- 低于 960px:左侧对话和右侧画布改为顶部按钮切换的单面板模式;节点说明仍使用覆盖式 Drawer。
|
|
||||||
- 两栏分隔条:支持 pointer 拖动、方向键、Home、End。
|
|
||||||
|
|
||||||
### 4.1 原生消息流与并行智能体切换
|
|
||||||
|
|
||||||
“复用 `MessageList`”在本页面的含义是复用它现有的 assistant 文本、`tool_calls`、`tool` 消息、引用、文件和协助卡渲染语义;不把 AgentScope 的运行过程先解析成专用步骤条、进度摘要或阶段结果卡。
|
|
||||||
|
|
||||||
单个智能体:
|
|
||||||
|
|
||||||
- `agent.reply.started` 立即创建一条稳定的 assistant 消息;`message.delta` 持续更新同一 `messageId`,所以用户会先看到流式状态、再看到逐字回复,而不是等待节点结束。
|
|
||||||
- 工具调用创建/更新该 assistant 消息的 `tool_calls`;工具返回创建匹配 `tool_call_id` 的原生 `tool` 消息。现有 `MessageGroup` 负责把两者配对显示,不再另写工具过程渲染器。
|
|
||||||
- AgentScope TeamSay 作为带 `agentRunId` 的普通 assistant 消息进入同一流,按到达顺序显示。它不是节点完成的证据,节点状态仍由右侧画布和后端状态机决定。
|
|
||||||
|
|
||||||
并行智能体:
|
|
||||||
|
|
||||||
```text
|
|
||||||
多角度并行研究 3/4 进行中
|
|
||||||
[ 协调者 ] [ 市场规模 ✓ ] [ 品牌竞争 ● ] [ 海外市场 ● ] [ 政策风险 ! ]
|
|
||||||
└──────────────────── 当前选中智能体的原生 MessageList ────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- `AgentStageSelector` 只在并行阶段出现,横向放置在左侧对话区顶部;使用现有图标体系的头像/角色图标、名称、状态文字和非纯颜色状态标识。默认选中第一位已开始运行的成员;用户手动切换后保持选择,不因其他成员返回而抢走阅读焦点。
|
|
||||||
- 所有成员的事件都持续接收、按每个 `agentRunId` 持久化和更新,即使当前未选中;切回即可看到完整历史与正在更新的同一条消息,不丢 token、不重新请求、不把结果拼成摘要。
|
|
||||||
- 当前选择只将共享用户消息、协调者广播消息和目标成员消息组成一个 `BaseStream<AgentThreadState>` 交给同一个 `MessageList`。切换标签仅切换该视图的消息范围,不改变 run、节点或消息的后端事实。
|
|
||||||
- 某成员请求用户协助时,其标签显示“待回复”徽标,选择器显示待处理数;点击徽标切到该成员,原生协助工具消息仍由现有 `MessageList` 协助卡渲染。其他成员可继续运行。
|
|
||||||
- 节点抽屉打开某并行成员时,同步聚焦对应标签;抽屉仍只展示合同。用户从抽屉或输入框发送反馈时带 `targetNodeId`/`agentRunId`,后端在安全点生效。
|
|
||||||
|
|
||||||
这不是把完整日志另做成调试面板:对话区展示的是模型和工具的真实业务消息;只有 chain-of-thought、内部提示词及按权限必须脱敏的字段不进入该消息流。
|
|
||||||
|
|
||||||
### 4.2 页面主要状态
|
|
||||||
|
|
||||||
| 状态 | 左侧对话区 | 右侧流程画布 | 主要操作 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `empty` | 欢迎语和输入框 | 空态 | 输入需求、选模板 |
|
|
||||||
| `clarifying` | 澄清问题和协助卡 | 需求理解节点 | 回答或补充 |
|
|
||||||
| `planning` | 用户消息和规划进度 | 骨架或需求图 | 继续补充,等待安全点生效 |
|
|
||||||
| `proposal_ready` | 2~3 张方案卡 | 点击卡片预览对应图 | 切换、修改、采用 |
|
|
||||||
| `running` | 协调者与成员原生消息、工具过程和用户反馈 | 实时节点状态 | 干预、停止、在对话中要求重跑 |
|
|
||||||
| `awaiting_input` | 等待用户卡片、原因和影响 | 对应节点显示等待 | 回答、取消 |
|
|
||||||
| `reviewing` | 审稿意见和返工过程 | 评审/返工节点状态 | 接受或补充要求 |
|
|
||||||
| `completed` | 总结、报告文件卡、版本和后续对话 | 完整执行图 | 问答、改写、导出 |
|
|
||||||
| `failed` | 错误说明和恢复建议 | 失败节点 | 通过对话重试或取消 |
|
|
||||||
| `cancelled` | 已保留成果说明 | 未完成节点取消 | 从已有成果继续或新建方案 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 前端目录与组件边界
|
|
||||||
|
|
||||||
建议新增:
|
|
||||||
|
|
||||||
```text
|
|
||||||
frontend-web/src/report-collaboration/
|
|
||||||
api/
|
|
||||||
client.ts
|
|
||||||
sessions.ts
|
|
||||||
plans.ts
|
|
||||||
runs.ts
|
|
||||||
reports.ts
|
|
||||||
types.ts
|
|
||||||
components/
|
|
||||||
ReportCollaborationLayout.tsx
|
|
||||||
ReportCollaborationHeader.tsx
|
|
||||||
ConversationSidebarTabs.tsx
|
|
||||||
ReportConversationPanel.tsx
|
|
||||||
AgentStageSelector.tsx
|
|
||||||
ReportComposer.tsx
|
|
||||||
ReportPlanCards.tsx
|
|
||||||
ReportFlowCanvas.tsx
|
|
||||||
ReportFlowNode.tsx
|
|
||||||
NodeInspectorDrawer.tsx
|
|
||||||
ReportPreviewDialog.tsx
|
|
||||||
InterventionCard.tsx
|
|
||||||
SessionHistoryPanel.tsx
|
|
||||||
ConnectionStatusBar.tsx
|
|
||||||
hooks/
|
|
||||||
useReportCollaborationSession.ts
|
|
||||||
useReportCollaborationStream.ts
|
|
||||||
useReportCollaborationMessages.ts
|
|
||||||
useReportPlanSelection.ts
|
|
||||||
useReportPanelLayout.ts
|
|
||||||
state/
|
|
||||||
event-normalizer.ts
|
|
||||||
event-reducer.ts
|
|
||||||
selectors.ts
|
|
||||||
message-adapter.ts
|
|
||||||
pages/
|
|
||||||
ReportCollaborationPage.tsx
|
|
||||||
styles/
|
|
||||||
report-collaboration.css
|
|
||||||
tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
修改范围:
|
|
||||||
|
|
||||||
- `frontend-web/src/pages/WorkspaceRoutes.tsx`:增加页面和会话路由。
|
|
||||||
- `frontend-web/src/components/workspace/workspace-nav-chat-list.tsx`:增加导航入口。
|
|
||||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`:仅允许增加一个向后兼容的通用业务卡片插槽。
|
|
||||||
- `frontend-web/src/core/messages/utils.ts`:如确需识别通用业务卡类型,只做集中式扩展。
|
|
||||||
|
|
||||||
不得把新功能放进:
|
|
||||||
|
|
||||||
- `frontend-web/src/roundtable-planning/`
|
|
||||||
- `frontend-web/src/open-canvas/`
|
|
||||||
- Coze 项目的 `workflow-standalone/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 数据来源与前端状态
|
|
||||||
|
|
||||||
### 6.1 三层状态
|
|
||||||
|
|
||||||
前端状态分为:
|
|
||||||
|
|
||||||
1. 服务器快照:session、原生 messages、plans、run、nodes、artifacts、report versions;每条消息带稳定的 `agentRunId`、`nodeRunId`、`phaseId` 和可见性标记,其中 artifacts 只用于生成左侧消息卡和报告文件,不形成独立结果面板。
|
|
||||||
2. durable SSE reducer:从 `seq` 连续应用事件,更新服务器快照投影。
|
|
||||||
3. 纯展示状态:预览中的方案、选中的节点、聚焦的并行 `agentRunId`、两栏宽度、节点抽屉开关、窄屏当前视图和画布位置。
|
|
||||||
|
|
||||||
纯展示状态不能改变执行结果。点击卡片只改变 `previewPlanId`;只有点击“采用方案”才改变服务器 `selectedPlanId`。
|
|
||||||
|
|
||||||
### 6.2 前端事件包
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface CollaborationEvent<T = unknown> {
|
|
||||||
eventId: string;
|
|
||||||
sessionId: string;
|
|
||||||
runId?: string;
|
|
||||||
seq: number;
|
|
||||||
type: string;
|
|
||||||
timestamp: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
必须处理:
|
|
||||||
|
|
||||||
- 快照先到、事件后到。
|
|
||||||
- 事件重复。
|
|
||||||
- 事件短暂乱序。
|
|
||||||
- SSE 断线后从 `after_seq` 重放。
|
|
||||||
- 页面切换时旧连接晚到事件。
|
|
||||||
- token delta 多次更新同一消息。
|
|
||||||
- TeamSay、assistant 文本、tool-call 参数 delta、tool 返回 delta 分别更新各自稳定消息,不得等节点完成后批量生成摘要。
|
|
||||||
- 并行成员的消息可交错到达;Reducer 必须按 `agentRunId + messageId` 更新,而不是按当前被选中的标签丢弃未聚焦成员的事件。
|
|
||||||
- 节点重新打开后出现新 attempt。
|
|
||||||
|
|
||||||
Reducer 以 `sessionId + runId + seq` 去重;消息行以 `messageId`、工具结果以 `toolCallId`、标签以 `agentRunId` 使用后端稳定 ID,禁止使用数组下标。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 分步实施计划
|
|
||||||
|
|
||||||
### FE-0:冻结参考视觉与行为基线
|
|
||||||
|
|
||||||
任务:
|
|
||||||
|
|
||||||
1. 在 1440×900、1280×800、1024×768 打开参考页面。
|
|
||||||
2. 保存初始态、方案态、运行态和节点说明抽屉四组截图。
|
|
||||||
3. 记录顶栏、两栏比例、间距、圆角、边框、阴影、画布工具栏和状态色。
|
|
||||||
4. 形成“参考组件—目标组件—允许差异”表。
|
|
||||||
5. 明确消息列表保持 DeerFlow 原样,不纳入像素对齐。
|
|
||||||
|
|
||||||
交付物:
|
|
||||||
|
|
||||||
- 视觉基线截图。
|
|
||||||
- 视觉 token 表。
|
|
||||||
- 页面行为清单。
|
|
||||||
|
|
||||||
验收:参考的每个用户可见状态都有截图或明确说明不存在。
|
|
||||||
|
|
||||||
### FE-1:路由、菜单和静态页面骨架
|
|
||||||
|
|
||||||
任务:
|
|
||||||
|
|
||||||
1. 增加两个路由和菜单项。
|
|
||||||
2. 创建 `ReportCollaborationPage`。
|
|
||||||
3. 创建顶栏、左侧对话栏、右侧画布和底部连接栏。
|
|
||||||
4. 实现可调整宽度的两栏布局,以及“专注对话/专注画布”切换。
|
|
||||||
5. 实现窄屏单面板切换与覆盖式节点 Drawer。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 不连接后端也能呈现完整空态。
|
|
||||||
- 375~1440px 不出现页面级横向滚动。
|
|
||||||
- 对话/画布切换不会卸载会话状态或清空输入内容。
|
|
||||||
- 键盘可以调整两栏分隔条。
|
|
||||||
|
|
||||||
### FE-2:页面设计系统与 Coze 风格重建
|
|
||||||
|
|
||||||
建立页面专用语义变量:
|
|
||||||
|
|
||||||
```text
|
|
||||||
--rc-bg-page
|
|
||||||
--rc-bg-panel
|
|
||||||
--rc-bg-canvas
|
|
||||||
--rc-border
|
|
||||||
--rc-text-primary
|
|
||||||
--rc-text-secondary
|
|
||||||
--rc-accent
|
|
||||||
--rc-running
|
|
||||||
--rc-waiting
|
|
||||||
--rc-success
|
|
||||||
--rc-danger
|
|
||||||
--rc-panel-shadow
|
|
||||||
--rc-card-radius
|
|
||||||
```
|
|
||||||
|
|
||||||
规则:
|
|
||||||
|
|
||||||
- 从 DeerFlow 主题 token 派生,不在组件中散落十六进制颜色。
|
|
||||||
- 使用 Lucide SVG 图标,不使用 emoji。
|
|
||||||
- hover/focus/展开动画控制在 150~300ms。
|
|
||||||
- 支持 `prefers-reduced-motion`。
|
|
||||||
- 交互状态不能只靠颜色表达。
|
|
||||||
- 正文对比度达到 4.5:1。
|
|
||||||
- 操作超过 300ms 显示 spinner、骨架或流式状态。
|
|
||||||
|
|
||||||
验收:除 MessageList 内部外,两栏布局、密度、卡片、画布工具栏、弹窗和节点抽屉达到参考基线。
|
|
||||||
|
|
||||||
### FE-3:API 类型和 TanStack Query 层
|
|
||||||
|
|
||||||
实现:
|
|
||||||
|
|
||||||
- session CRUD。
|
|
||||||
- messages 查询与发送。
|
|
||||||
- plan request、plans 查询、select。
|
|
||||||
- run 创建、查询、停止、重试。
|
|
||||||
- command 创建、确认、取消。
|
|
||||||
- artifacts、sources、reports、versions 查询。
|
|
||||||
|
|
||||||
每个写请求携带:
|
|
||||||
|
|
||||||
- `idempotencyKey`
|
|
||||||
- `expectedRevision`
|
|
||||||
|
|
||||||
冲突行为:
|
|
||||||
|
|
||||||
- `409`:刷新当前快照并提示用户重新确认。
|
|
||||||
- `422`:在对应操作附近显示具体错误。
|
|
||||||
- `429`:显示预算/并发限制,不自动重试。
|
|
||||||
- `5xx`:保留用户输入,允许使用同一幂等键重试。
|
|
||||||
|
|
||||||
验收:API 层不包含页面展示逻辑,类型与后端 OpenAPI 一致。
|
|
||||||
|
|
||||||
### FE-4:SSE、快照和事件 Reducer
|
|
||||||
|
|
||||||
实现 `useReportCollaborationStream`:
|
|
||||||
|
|
||||||
1. 先拉 session snapshot。
|
|
||||||
2. 使用 snapshot 的 `lastSeq` 建立 SSE。
|
|
||||||
3. 校验每个事件 session/run 身份。
|
|
||||||
4. 按 seq 应用事件并去重。
|
|
||||||
5. 出现序号缺口时暂停 live apply,补拉缺失事件。
|
|
||||||
6. 断线指数退避并支持 `Last-Event-ID`。
|
|
||||||
7. 终态后完成最后一次事件补拉再关闭连接。
|
|
||||||
|
|
||||||
Reducer 必须覆盖:
|
|
||||||
|
|
||||||
- 消息流增量。
|
|
||||||
- 计划候选和 revision。
|
|
||||||
- node/node attempt 状态。
|
|
||||||
- artifact 创建和校验。
|
|
||||||
- command 分类和执行。
|
|
||||||
- report delta 和正式版本。
|
|
||||||
- 错误、取消、接管恢复。
|
|
||||||
- AgentScope 原始可见消息和工具事件:`message.created/delta/completed`、`tool_call.created/delta/completed`、`tool_result.created/delta/completed`、`team_message.created/delta/completed`。
|
|
||||||
|
|
||||||
验收:重复事件、乱序事件和重连都不产生重复消息、重复节点或状态倒退。
|
|
||||||
|
|
||||||
### FE-5:复用 DeerFlow `MessageList`
|
|
||||||
|
|
||||||
复用:
|
|
||||||
|
|
||||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`
|
|
||||||
- `frontend-web/src/components/workspace/messages/message-list-item.tsx`
|
|
||||||
- `frontend-web/src/components/workspace/messages/message-group.tsx`
|
|
||||||
- `frontend-web/src/components/workspace/messages/context.ts`
|
|
||||||
|
|
||||||
实现无损协议适配 `message-adapter.ts`:
|
|
||||||
|
|
||||||
1. 将已持久化的 `CollaborationMessage` 一对一映射为 LangGraph SDK `Message[]`,不将文本或工具结果二次概括。
|
|
||||||
2. 构造最小 `BaseStream<AgentThreadState>` 外观:`messages`、`isLoading`、`isThreadLoading`、`values`。
|
|
||||||
3. 协调者、成员 AgentScope TeamSay 和成员回复都映射为普通 assistant 消息;保留 `messageId`、文本 chunk、角色/成员名称、`agentRunId`、`nodeRunId`、`phaseId`、来源事件 ID 和完成态。
|
|
||||||
4. AgentScope 工具调用映射为该 assistant 消息的标准 `tool_calls`;调用结果映射为标准 `tool` 消息,并严格复用原有 `tool_call_id` 配对。工具参数、返回正文及增量保持原文,只有后端明确标注的脱敏字段被替换。
|
|
||||||
5. 将来源、文件、报告 Markdown delta 和正式版本继续映射为既有引用、文件卡和普通 assistant 消息;规划卡、用户确认卡和报告替换卡仍通过一个通用业务卡片插槽渲染。
|
|
||||||
6. 错误、恢复建议、命令影响范围和报告版本变化也必须进入同一时间线;错误堆栈以可读错误消息替代,但不能吞掉已发生的工具/成员消息。
|
|
||||||
7. `selectMessagesForAgentRun(allMessages, focusedAgentRunId)` 只在客户端从已同步消息中选出共享消息与目标成员消息;并行选择器不得重新拉取、重新排序或改写消息。
|
|
||||||
|
|
||||||
约束:
|
|
||||||
|
|
||||||
- 不使用 LangGraph `useThreadStream` 发送消息。
|
|
||||||
- 新页面输入框调用报告协作 API。
|
|
||||||
- 不增加多个页面专用 MessageList props。
|
|
||||||
- 普通聊天、AI 写作、深度研究等未传插槽时必须零变化。
|
|
||||||
- 不另建执行结果列表、节点结果面板或成果时间线。
|
|
||||||
- 不用“运行中”“已检索 N 条”等专用摘要替代真实消息。`MessageList` 本身的流式状态和原生工具步骤是唯一过程视图。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 历史和流式消息使用同一行稳定更新。
|
|
||||||
- 单成员首次开始回复、每次工具启动和工具返回均在 300ms 内出现可见的原生消息行或原生工具步骤;不允许持续数秒只有静态加载圈。
|
|
||||||
- 并行标签切换后显示目标成员的完整已持久化消息和实时 delta;未选中成员持续接收,标签上的运行/待回复状态及时更新。
|
|
||||||
- 用户上翻阅读时不强制滚底。
|
|
||||||
- 用户发送新消息后滚到最新位置。
|
|
||||||
- 卡片显示成功不改变后端节点状态。
|
|
||||||
|
|
||||||
### FE-6:需求澄清和候选方案卡片
|
|
||||||
|
|
||||||
`ReportPlanCards` 展示:
|
|
||||||
|
|
||||||
- 推荐标签、策略、标题、摘要和推荐理由。
|
|
||||||
- 分析角度和参与角色。
|
|
||||||
- 关键步骤与质量门。
|
|
||||||
- 预计时间、成本等级和资料范围。
|
|
||||||
- 方案验证错误。
|
|
||||||
|
|
||||||
交互:
|
|
||||||
|
|
||||||
- 点击卡片或“查看流程”只切换预览。
|
|
||||||
- “采用此方案”调用 select API。
|
|
||||||
- “开始协作”再次校验并创建 run。
|
|
||||||
- 方案调整后显示新 revision;旧 revision 只读。
|
|
||||||
- 过期 revision 返回 409 后不静默覆盖。
|
|
||||||
|
|
||||||
验收:连续快速切换三张卡时,画布最终显示最后一次选择,不闪回旧图。
|
|
||||||
|
|
||||||
### FE-7:协作流程画布
|
|
||||||
|
|
||||||
技术:`@xyflow/react`。可以借鉴但不直接耦合:
|
|
||||||
|
|
||||||
- `frontend-web/src/components/ai-elements/canvas.tsx`
|
|
||||||
- `frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx`
|
|
||||||
|
|
||||||
节点显示:
|
|
||||||
|
|
||||||
- 节点名、角色名和分析角度。
|
|
||||||
- 状态文字、图标和颜色。
|
|
||||||
- 等待用户、返工、失败和 superseded 标记。
|
|
||||||
|
|
||||||
画布能力:
|
|
||||||
|
|
||||||
- 自动布局。
|
|
||||||
- 拖动节点只保存浏览器展示位置。
|
|
||||||
- 缩放、适配视图、方向切换、小地图。
|
|
||||||
- 活动边低强度流动动画。
|
|
||||||
- 点击节点打开覆盖在画布上的节点说明抽屉。
|
|
||||||
- 节点重跑、补充或纠偏一律通过左侧对话完成;抽屉只提供“在对话中对此节点提出意见”入口。
|
|
||||||
- 方案切换后清理不属于新方案的本地坐标。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 30 个节点内流畅操作。
|
|
||||||
- 单个 node event 不导致整个 React Flow 重挂。
|
|
||||||
- `awaiting_input`、`reopened`、`superseded` 不与 `completed` 混淆。
|
|
||||||
|
|
||||||
### FE-8:节点说明抽屉与对话定位
|
|
||||||
|
|
||||||
点击画布节点打开 `NodeInspectorDrawer`。抽屉只展示计划合同:
|
|
||||||
|
|
||||||
1. 节点名称、角色、分析角度和当前状态标签。
|
|
||||||
2. 节点职责:负责解决什么问题。
|
|
||||||
3. 要根据什么:所需输入类型、上游节点类型、资料范围和时效要求。
|
|
||||||
4. 要产出什么:成果类型、结构要求和验收条件。
|
|
||||||
5. 可用工具、禁止事项、预算上限。
|
|
||||||
6. 上游与下游关系。
|
|
||||||
|
|
||||||
抽屉不展示:
|
|
||||||
|
|
||||||
- 节点实际生成内容。
|
|
||||||
- 工具调用返回值。
|
|
||||||
- EvidenceBundle 或其他成果物正文。
|
|
||||||
- attempt 执行时间线。
|
|
||||||
- 错误堆栈和调试信息。
|
|
||||||
- 报告正文。
|
|
||||||
|
|
||||||
交互:
|
|
||||||
|
|
||||||
- “在对话中对此节点提出意见”关闭抽屉、聚焦左侧 Composer,并绑定 `targetNodeId` 上下文。
|
|
||||||
- 若该节点对应正在并行运行的成员,同时绑定该成员的 `agentRunId` 并切换左侧选择器,使用户发送前能先看到该成员的真实对话与工具过程。
|
|
||||||
- 用户仍需在对话区输入“重新检索”“重新分析”等要求;抽屉不直接发起业务变更。
|
|
||||||
- 状态标签来自后端节点状态,但不加载节点结果接口。
|
|
||||||
- 关闭抽屉不清空 selected node;再次点击同一节点可恢复说明。
|
|
||||||
|
|
||||||
报告预览不使用常驻第三栏。最终报告、章节候选和版本先作为左侧 MessageList 中的普通消息/文件卡出现;点击文件卡时复用 `ArtifactFileDetail` 或现有 Markdown 能力打开大尺寸 Dialog,关闭后回到两栏工作台。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 抽屉宽度不改变两栏布局,不挤压左侧对话区。
|
|
||||||
- 抽屉只读取节点合同和图结构,不请求节点执行结果。
|
|
||||||
- 所有原生成员消息、工具过程、错误和报告入口都能在左侧对话时间线找到。
|
|
||||||
|
|
||||||
### FE-9:全过程对话干预
|
|
||||||
|
|
||||||
`ReportComposer` 根据状态显示不同提示:
|
|
||||||
|
|
||||||
- 空闲:输入报告需求。
|
|
||||||
- 规划中:补充要求将在规划安全点应用。
|
|
||||||
- 运行中:协调者正在判断影响范围。
|
|
||||||
- 等待用户:优先回答等待问题。
|
|
||||||
- 完成后:可问答、重写、润色或重新研究。
|
|
||||||
|
|
||||||
发送流程:
|
|
||||||
|
|
||||||
1. 生成 idempotency key。
|
|
||||||
2. 插入 pending 用户消息;带 `targetNodeId`/`agentRunId` 时在相关成员消息流和共享上下文中引用同一条用户消息,不重复持久化。
|
|
||||||
3. 显示“正在识别修改范围”。
|
|
||||||
4. `command.classified` 后展示意图和影响节点。
|
|
||||||
5. 高成本或低置信度命令显示确认卡。
|
|
||||||
6. `command.accepted` 后显示排队或等待安全点。
|
|
||||||
7. 完成后关联新 node attempt 或 report version。
|
|
||||||
8. 失败时保留原消息和重试入口。
|
|
||||||
|
|
||||||
典型映射:
|
|
||||||
|
|
||||||
| 用户输入 | 预期操作 |
|
|
||||||
|---|---|
|
|
||||||
| “增加政策风险角度” | 新增角度节点和受影响下游 |
|
|
||||||
| “资料太旧,重新找” | 重新检索指定证据包 |
|
|
||||||
| “竞争格局分析不到位” | 复用证据,重新分析 |
|
|
||||||
| “重写第二章” | 只生成章节替换候选 |
|
|
||||||
| “语气更正式” | 新报告版本,不重新研究 |
|
|
||||||
| “为什么得出这个结论” | 只读问答,不修改报告 |
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 双击发送不创建重复命令。
|
|
||||||
- 选中“品牌竞争分析师”后发送补充要求,只影响该成员及依赖分析,其他并行成员仍保持消息流和运行状态。
|
|
||||||
- 运行中反馈不会让 run 或 node 误变 completed。
|
|
||||||
- “重写第二章”不会触发全量检索。
|
|
||||||
- “资料太旧”不会只做文字润色。
|
|
||||||
|
|
||||||
### FE-10:会话历史、报告版本与导出
|
|
||||||
|
|
||||||
- History Tab 显示标题、状态、更新时间、当前版本。
|
|
||||||
- 新建会话不删除旧会话。
|
|
||||||
- 会话切换时先关闭旧 SSE,再加载新快照。
|
|
||||||
- 报告修改先生成候选,确认后创建新版本。
|
|
||||||
- 版本恢复创建新的头版本,不删除历史。
|
|
||||||
- 下载复用现有 Markdown/Word 能力。
|
|
||||||
- 文件名来自报告标题,不暴露宿主机或沙箱路径。
|
|
||||||
|
|
||||||
验收:历史会话可完整恢复左侧消息流程、右侧节点状态和报告版本;不额外恢复节点结果面板。
|
|
||||||
|
|
||||||
### FE-11:测试、视觉回归和性能
|
|
||||||
|
|
||||||
单元测试:
|
|
||||||
|
|
||||||
- event normalizer/reducer。
|
|
||||||
- message adapter。
|
|
||||||
- plan revision 和选择状态。
|
|
||||||
- node status 和 available actions。
|
|
||||||
- command pending/idempotency。
|
|
||||||
|
|
||||||
组件测试:
|
|
||||||
|
|
||||||
- 空态、澄清、规划、候选、运行、等待、完成、失败、取消。
|
|
||||||
- 卡片选择、节点点击、只读说明 Drawer、确认弹窗和报告预览 Dialog。
|
|
||||||
- MessageList 通用业务卡插槽零回归。
|
|
||||||
- 单成员流式文本、TeamSay、工具调用与工具返回的原样重放;并行成员交错到达、切换标签、切回后不中断 delta。
|
|
||||||
- 非当前标签成员发起协助时显示待回复徽标,点击后使用原生协助卡回复到正确节点。
|
|
||||||
|
|
||||||
E2E:
|
|
||||||
|
|
||||||
1. 新建会话并输入新能源汽车报告要求。
|
|
||||||
2. 选择候选并开始。
|
|
||||||
3. 运行中增加风险角度。
|
|
||||||
4. 重新检索海外最新资料。
|
|
||||||
5. 完成后重写第二章。
|
|
||||||
6. 刷新、断网重连、切换会话。
|
|
||||||
7. 停止和重试失败节点。
|
|
||||||
|
|
||||||
性能门:
|
|
||||||
|
|
||||||
- 30 节点画布不卡顿。
|
|
||||||
- 长消息列表使用现有优化策略。
|
|
||||||
- SSE 高频 delta 合帧后再 setState。
|
|
||||||
- 画布和消息区使用独立 selector,避免互相触发整树重渲染。
|
|
||||||
- 节点抽屉只订阅节点合同和状态,不能因阶段产出更新而反复重渲染。
|
|
||||||
- 只渲染当前聚焦成员和共享消息的可视窗口;非当前成员的消息仍写入 store,但不因 token delta 重渲染当前 `MessageList`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 后端接口依赖
|
|
||||||
|
|
||||||
前端开发前,后端必须冻结以下接口:
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST /api/report-collaboration/sessions
|
|
||||||
GET /api/report-collaboration/sessions
|
|
||||||
GET /api/report-collaboration/sessions/{session_id}
|
|
||||||
PATCH /api/report-collaboration/sessions/{session_id}
|
|
||||||
DELETE /api/report-collaboration/sessions/{session_id}
|
|
||||||
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/messages
|
|
||||||
GET /api/report-collaboration/sessions/{session_id}/messages
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/plan-requests
|
|
||||||
GET /api/report-collaboration/sessions/{session_id}/plans
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/plans/{plan_id}/select
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/runs
|
|
||||||
|
|
||||||
GET /api/report-collaboration/runs/{run_id}
|
|
||||||
GET /api/report-collaboration/runs/{run_id}/events
|
|
||||||
GET /api/report-collaboration/runs/{run_id}/stream
|
|
||||||
POST /api/report-collaboration/runs/{run_id}/commands
|
|
||||||
POST /api/report-collaboration/runs/{run_id}/cancel
|
|
||||||
GET /api/report-collaboration/runs/{run_id}/nodes/{node_id}/contract
|
|
||||||
|
|
||||||
GET /api/report-collaboration/sessions/{session_id}/reports
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/report-rewrites
|
|
||||||
POST /api/report-collaboration/sessions/{session_id}/report-rewrites/{id}/apply
|
|
||||||
```
|
|
||||||
|
|
||||||
前后端共享枚举:
|
|
||||||
|
|
||||||
- session status。
|
|
||||||
- run status。
|
|
||||||
- node/node attempt status。
|
|
||||||
- command intent/status。
|
|
||||||
- event type/version。
|
|
||||||
- artifact type/schema version。
|
|
||||||
- report version/change type。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 前端开发顺序
|
|
||||||
|
|
||||||
页面与契约层已完成(代码在 `frontend-web/src/report-collaboration/`):
|
|
||||||
|
|
||||||
1. `RC-FE-001`:视觉截图与行为基线。
|
|
||||||
2. `RC-FE-002`:路由、菜单和左对话/右画布两栏空壳。
|
|
||||||
3. `RC-FE-003`:页面 token、弹窗、抽屉和响应式。
|
|
||||||
4. `RC-FE-004`:API 类型和 mock server。
|
|
||||||
5. `RC-FE-005`:SSE reducer 与快照恢复。
|
|
||||||
6. `RC-FE-006`:MessageList adapter 和通用卡片插槽。
|
|
||||||
7. `RC-FE-007`:需求澄清与候选方案卡。
|
|
||||||
8. `RC-FE-008`:流程画布和自定义节点。
|
|
||||||
9. `RC-FE-009`:只读节点说明抽屉与对话目标定位。
|
|
||||||
10. `RC-FE-010`:统一 Composer 和命令确认。
|
|
||||||
11. `RC-FE-011`:报告预览、版本和导出。
|
|
||||||
12. `RC-FE-012`:历史、重连和异常恢复。
|
|
||||||
13. `RC-FE-013`:组件/E2E/视觉/性能验收。
|
|
||||||
|
|
||||||
依赖规则:
|
|
||||||
|
|
||||||
- 契约已冻结为 `src/report-collaboration/api/types.ts`;后端 `RC-BE-001` 已对齐。
|
|
||||||
- 真实 SSE 联调:后端 `RC-BE-011` 已挂载 `GET /runs/{id}/stream`;默认无后台循环时流只会重放已入库事件。
|
|
||||||
- 报告版本操作已接后端 `RC-BE-015`(改写候选 / apply / restore)。
|
|
||||||
- 执行画布实时状态依赖打开 `worker_enabled`;质量角色内核已在 `RC-BE-013` 落地,真实成员见 `RC-BE-017`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 前端完成定义
|
|
||||||
|
|
||||||
页面与 mock 联调路径已落地。真实后端可联调 `GET /runs/{id}/stream`、领取/恢复(`RC-BE-012`,默认不挂 worker)、质量角色(`RC-BE-013`)以及报告改写/恢复(`RC-BE-015`)。此前以 mock driver 覆盖交互,不以「前端未编码」计未完成。
|
|
||||||
|
|
||||||
前端只有同时满足以下条件才算完成:
|
|
||||||
|
|
||||||
1. 页面结构与 Coze 参考页面逐状态验收通过。
|
|
||||||
2. 消息区复用 DeerFlow `MessageList`,其他聊天页面无回归。
|
|
||||||
3. 候选卡、左侧对话流程、右侧流程图和报告来自同一 session/run/event 状态。
|
|
||||||
4. 点击候选只预览,明确确认后才执行。
|
|
||||||
5. 用户可以在规划、运行、完成后三个阶段继续对话。
|
|
||||||
6. 协助卡显示或提交不会导致前端误标完成。
|
|
||||||
7. 节点点击只打开说明抽屉,可查看职责、所需输入、预期输出、工具和验收条件,不展示执行结果。
|
|
||||||
8. 节点重开、返工和 superseded 状态表达正确。
|
|
||||||
9. SSE 重连、刷新和历史切换无重复消息和状态倒退。
|
|
||||||
10. 报告修改以候选和版本形式完成,不直接覆盖正式报告。
|
|
||||||
11. 页面始终只有对话与画布两个常驻区域,所有执行过程和产出入口都能在对话时间线中找到。
|
|
||||||
12. 响应式、键盘、对比度、加载反馈和错误恢复通过验收。
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
# 报告协作工作台 — 前端对接指南
|
|
||||||
|
|
||||||
> **适用版本:** RC-BE-000~018 已落地(2026-09-06)
|
|
||||||
> **配套文档:**
|
|
||||||
> - [前端实施计划](./agentscope-多智能体报告协作工作台-前端实施计划.md)
|
|
||||||
> - [后端实施计划](./agentscope-多智能体报告协作工作台-后端实施计划.md)
|
|
||||||
> - 模块 README:`frontend-web/src/report-collaboration/README.md`
|
|
||||||
|
|
||||||
本文说明**前端如何对接真实后端**:环境配置、API 契约、用户流程、状态机、错误处理与联调验收。前端 API 客户端与页面已实现,通常**无需再写一套 HTTP 层**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 结论速览
|
|
||||||
|
|
||||||
| 问题 | 答案 |
|
|
||||||
|------|------|
|
|
||||||
| 前端还要写接口吗? | **不用**。`src/report-collaboration/api/` 已封装 REST + SSE,页面通过 `getReportCollaborationDriver()` 调用。 |
|
|
||||||
| 为什么调不通? | 最常见:`config.yaml` 里 `report_collaboration.enabled: false` → 全部 **503**;或 `worker_enabled: false` → run 不入队执行。 |
|
|
||||||
| Mock 和真后端怎么切? | **Mock 已移除**。前端永远请求真实后端(`deerflow.rc-mock` / `VITE_RC_MOCK` 开关已删除);联调需 `enabled: true`。 |
|
|
||||||
| 字段会和 MySQL 对不上吗? | 前端只认 JSON 契约(`api/types.ts`),不直连表。缺列表现为后端 500/SQL 错误,部署后需**重启 Gateway** 跑 `create_all` + 列同步。 |
|
|
||||||
| 从哪进页面? | `/page/workspace/report-collaboration`(新会话)或 `/page/workspace/report-collaboration/:sessionId`。侧边栏入口默认注释,可直接 URL 访问。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 架构:前端已有什么
|
|
||||||
|
|
||||||
```
|
|
||||||
ReportCollaborationPage
|
|
||||||
└─ useReportCollaborationSession(唯一数据入口)
|
|
||||||
├─ driver.getSnapshot() ← REST 冷启动
|
|
||||||
├─ driver.openRunStream() ← SSE 实时事件
|
|
||||||
├─ driver.sendMessage / createCommand / startRun …
|
|
||||||
└─ event-reducer ← 事件 → UI 状态
|
|
||||||
|
|
||||||
ReportConversationPanel
|
|
||||||
└─ MessageList(message-adapter 映射 CollaborationMessage → LangGraph Message)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.1 目录结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
frontend-web/src/report-collaboration/
|
|
||||||
api/
|
|
||||||
types.ts # 冻结契约(snake_case,与后端 Pydantic 一致)
|
|
||||||
client.ts # rcFetch:鉴权、幂等键、乐观锁、错误映射
|
|
||||||
sessions.ts # 会话 CRUD、消息
|
|
||||||
plans.ts # 方案请求 / 列表 / 选定
|
|
||||||
runs.ts # Run、SSE、命令、节点合同
|
|
||||||
reports.ts # 报告版本、改写、恢复
|
|
||||||
driver.ts # 真实后端统一门面(Mock 驱动已移除)
|
|
||||||
state/
|
|
||||||
event-normalizer.ts
|
|
||||||
event-reducer.ts
|
|
||||||
selectors.ts
|
|
||||||
message-adapter.ts
|
|
||||||
hooks/
|
|
||||||
useReportCollaborationSession.ts # 快照 → SSE → reducer → 动作
|
|
||||||
useReportCollaborationStream.ts # 断线重连、after_seq 续接
|
|
||||||
useReportCollaborationMessages.ts
|
|
||||||
components/ …
|
|
||||||
pages/ReportCollaborationPage.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 事实源原则
|
|
||||||
|
|
||||||
- **冷启动:** `GET /sessions/{id}` 返回 `SessionSnapshot`(session、messages、plans、run、nodes、agent_runs、commands、report_versions、last_seq)。
|
|
||||||
- **运行中:** `GET /runs/{id}/stream?after_seq=N` 订阅 durable SSE。
|
|
||||||
- **断线 / 序号缺口:** `GET /runs/{id}/events?after_seq=N` REST 补拉。
|
|
||||||
- **澄清 / 规划阶段(尚无 run):** 每 2.5s 轮询快照(后端事件主要在 run 流上)。
|
|
||||||
|
|
||||||
纯展示状态(预览方案、聚焦成员、面板宽度)**不**写入服务器,仅存前端。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 后端启用与部署
|
|
||||||
|
|
||||||
### 3.1 必改配置
|
|
||||||
|
|
||||||
文件:`offline-backend-20260512/backend/config.yaml`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
report_collaboration:
|
|
||||||
enabled: false # 联调必须改为 true
|
|
||||||
worker_enabled: false # 要看成员执行必须 true
|
|
||||||
planner_model: null # 建议填可用模型名
|
|
||||||
coordinator_model: null
|
|
||||||
research_model: null
|
|
||||||
writer_model: null
|
|
||||||
reviewer_model: null
|
|
||||||
# … token_budget、max_concurrent_runs_per_user 等见同文件
|
|
||||||
```
|
|
||||||
|
|
||||||
| 配置 | 现象 |
|
|
||||||
|------|------|
|
|
||||||
| `enabled: false` | 所有路由 **503**,`code: REPORT_COLLABORATION_DISABLED` |
|
|
||||||
| `enabled: true`,`worker_enabled: false` | REST/SSE 可用,run 入队但不自动执行(测 API 可用,无成员流式输出) |
|
|
||||||
| 两者 `true` + 模型配置完整 | 完整联调 |
|
|
||||||
|
|
||||||
### 3.2 MySQL / 表结构
|
|
||||||
|
|
||||||
生产环境 Alembic 自动升级默认关闭,启动时走 `create_all` + `_ensure_orm_columns_sync`。
|
|
||||||
|
|
||||||
- 迁移脚本(可选手动跑):`20260905_01`、`20260906_01`、`20260907_01`
|
|
||||||
- ORM:`packages/harness/deerflow/persistence/report_collaboration/model.py`(13 张表)
|
|
||||||
- **部署后务必重启 Gateway**,观察日志是否有 `Failed to auto-add column report_collaboration_*`
|
|
||||||
|
|
||||||
已有 RC-003 时代旧表会通过列同步补上 `template_snapshot_json`、`budget_snapshot_json`、`usage_json`、`budget_exhausted_reason` 等可空列,并创建 `rewrite_proposals`、`audits`。
|
|
||||||
|
|
||||||
### 3.3 前端环境变量
|
|
||||||
|
|
||||||
| 变量 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `VITE_BACKEND_BASE_URL` | Gateway 地址,默认 `http://127.0.0.1:8001` |
|
|
||||||
|
|
||||||
请求经 `apiFetch`,自动携带登录 JWT(`credentials: "include"`)。**必须先登录 DeerFlow**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. API 基址与请求约定
|
|
||||||
|
|
||||||
### 4.1 Base URL
|
|
||||||
|
|
||||||
```text
|
|
||||||
{VITE_BACKEND_BASE_URL}/api/report-collaboration
|
|
||||||
```
|
|
||||||
|
|
||||||
代码常量:`REPORT_COLLABORATION_BASE()`(`api/client.ts`)。
|
|
||||||
|
|
||||||
### 4.2 请求头
|
|
||||||
|
|
||||||
| Header | 何时携带 | 说明 |
|
|
||||||
|--------|----------|------|
|
|
||||||
| `Authorization` | 全部 | 由 `apiFetch` 注入 |
|
|
||||||
| `X-Idempotency-Key` | 写操作 | 格式 `rc-{uuid}`;重复提交返回首次结果 |
|
|
||||||
| `X-Expected-Revision` | 选方案、开 run、改会话等 | 会话 `requirement_revision` 或方案 `revision` |
|
|
||||||
| `Accept: text/event-stream` | SSE | `openRunStream` |
|
|
||||||
|
|
||||||
### 4.3 错误响应格式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "REPORT_COLLABORATION_DISABLED",
|
|
||||||
"message": "报告协作工作台未启用",
|
|
||||||
"detail": { }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
前端映射(`api/client.ts`):
|
|
||||||
|
|
||||||
| HTTP | 异常类 | 典型场景 |
|
|
||||||
|------|--------|----------|
|
|
||||||
| 409 | `ConflictError` | revision 过期、active run 冲突 |
|
|
||||||
| 422 | `ValidationError` | 参数/语义错误、缺幂等键 |
|
|
||||||
| 429 | `RateLimitError` | 预算耗尽、每用户并发 run 上限 |
|
|
||||||
| 5xx | `ServerError` | 可带同一幂等键重试 |
|
|
||||||
| 0 | `NetworkError` | fetch 失败 |
|
|
||||||
|
|
||||||
### 4.4 完整路由清单(与前端 client 对齐)
|
|
||||||
|
|
||||||
| 方法 | 路径 | 前端模块 |
|
|
||||||
|------|------|----------|
|
|
||||||
| POST | `/sessions` | `sessions.createSession` |
|
|
||||||
| GET | `/sessions` | `sessions.listSessions` |
|
|
||||||
| GET | `/sessions/{id}` | `sessions.getSessionSnapshot` |
|
|
||||||
| PATCH | `/sessions/{id}` | `sessions.updateSession` |
|
|
||||||
| DELETE | `/sessions/{id}` | `sessions.deleteSession` |
|
|
||||||
| GET | `/sessions/{id}/messages` | `sessions.listMessages` |
|
|
||||||
| POST | `/sessions/{id}/messages` | `sessions.sendMessage` |
|
|
||||||
| POST | `/sessions/{id}/plan-requests` | `plans.requestPlans` |
|
|
||||||
| GET | `/sessions/{id}/plans` | `plans.listPlans` |
|
|
||||||
| POST | `/sessions/{id}/plans/{planId}/select` | `plans.selectPlan` |
|
|
||||||
| POST | `/sessions/{id}/runs` | `runs.startRun` |
|
|
||||||
| GET | `/sessions/{id}/reports` | `reports.listReportVersions` |
|
|
||||||
| POST | `/sessions/{id}/report-rewrites` | `reports.createRewrite` |
|
|
||||||
| POST | `/sessions/{id}/report-rewrites/{id}/apply` | `reports.applyRewrite` |
|
|
||||||
| POST | `/sessions/{id}/report-versions/{id}/restore` | `reports.restoreReportVersion` |
|
|
||||||
| GET | `/runs/{id}` | `runs.getRun` |
|
|
||||||
| GET | `/runs/{id}/events` | `runs.listRunEvents` |
|
|
||||||
| GET | `/runs/{id}/stream` | `runs.openRunStream` |
|
|
||||||
| POST | `/runs/{id}/cancel` | `runs.cancelRun` |
|
|
||||||
| POST | `/runs/{id}/commands` | `runs.createCommand` |
|
|
||||||
| POST | `/runs/{id}/commands/{id}/confirm` | `runs.confirmCommand` |
|
|
||||||
| POST | `/runs/{id}/commands/{id}/cancel` | `runs.cancelCommand` |
|
|
||||||
| GET | `/runs/{id}/nodes/{nodeId}/contract` | `runs.getNodeContract` |
|
|
||||||
| GET | `/runs/{id}/artifacts` | (后端已暴露,前端按需接) |
|
|
||||||
| GET | `/runs/{id}/sources` | (后端已暴露,前端按需接) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 用户流程与 API 时序
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant U as 用户
|
|
||||||
participant FE as 前端 RC
|
|
||||||
participant API as Gateway
|
|
||||||
participant W as Worker
|
|
||||||
|
|
||||||
U->>FE: 输入需求
|
|
||||||
FE->>API: POST /sessions
|
|
||||||
FE->>API: POST /sessions/{id}/messages
|
|
||||||
API-->>FE: 澄清(快照 messages)
|
|
||||||
U->>FE: 回答澄清
|
|
||||||
FE->>API: POST /messages
|
|
||||||
FE->>API: POST /plan-requests
|
|
||||||
API-->>FE: 候选方案(快照 plans)
|
|
||||||
U->>FE: 采用方案
|
|
||||||
FE->>API: POST /plans/{id}/select
|
|
||||||
U->>FE: 开始协作
|
|
||||||
FE->>API: POST /runs
|
|
||||||
FE->>API: GET /runs/{id}/stream
|
|
||||||
W-->>API: 持久化事件
|
|
||||||
API-->>FE: SSE
|
|
||||||
U->>FE: 运行中干预
|
|
||||||
FE->>API: POST /commands → confirm
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.1 阶段 0:进入页面
|
|
||||||
|
|
||||||
| 路由 | 行为 |
|
|
||||||
|------|------|
|
|
||||||
| `/page/workspace/report-collaboration` | 空态;`POST /sessions` 后跳转带 `sessionId` |
|
|
||||||
| `/page/workspace/report-collaboration/:sessionId` | 加载快照;有 `location.state.rcInitialMessage` 时自动发首条消息 |
|
|
||||||
|
|
||||||
路由注册:`frontend-web/src/pages/WorkspaceRoutes.tsx`。
|
|
||||||
侧边栏入口已在 `workspace-nav-chat-list.tsx` 中放开(「报告协作」项),也可直接访问 URL。
|
|
||||||
|
|
||||||
### 5.2 阶段 1:需求澄清(无 run)
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/report-collaboration/sessions
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/messages
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{ "text": "请写一份…", "target_node_id": null, "agent_run_id": null }
|
|
||||||
```
|
|
||||||
|
|
||||||
- `useReportCollaborationSession.send()`:当 `session.status` 不是 `running` / `awaiting_input` / `reviewing` 时走 `sendMessage`。
|
|
||||||
- 会话状态:`empty` → `clarifying` → `planning` → `proposal_ready`。
|
|
||||||
- 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。
|
|
||||||
|
|
||||||
### 5.3 阶段 2:候选方案
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/plan-requests
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select
|
|
||||||
```
|
|
||||||
|
|
||||||
- **预览方案**(`previewPlanId`):纯前端,不调 API。
|
|
||||||
- **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。
|
|
||||||
- 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。
|
|
||||||
- 右栏 `ReportFlowCanvas` 使用 `PlanNode` / `PlanEdge` 渲染 DAG。
|
|
||||||
|
|
||||||
### 5.4 阶段 3:开始执行与 SSE
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/runs
|
|
||||||
{ "plan_id": "plan-xxx" }
|
|
||||||
|
|
||||||
GET /api/report-collaboration/runs/{runId}/stream?after_seq=0
|
|
||||||
Accept: text/event-stream
|
|
||||||
```
|
|
||||||
|
|
||||||
SSE 每条 `data:` 为 JSON:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface CollaborationEvent {
|
|
||||||
eventId: string;
|
|
||||||
sessionId: string;
|
|
||||||
runId?: string | null;
|
|
||||||
seq: number;
|
|
||||||
type: CollaborationEventType;
|
|
||||||
timestamp: string;
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
常用 `type`(完整列表见 `api/types.ts`):
|
|
||||||
|
|
||||||
| 类型 | 含义 |
|
|
||||||
|------|------|
|
|
||||||
| `message.created` / `message.delta` / `message.completed` | 对话镜像流式 |
|
|
||||||
| `tool_call.*` / `tool_result.*` | 工具调用与返回 |
|
|
||||||
| `team_message.*` | 成员 / 协调者广播 |
|
|
||||||
| `plan.proposed` / `plan.selected` | 方案生命周期 |
|
|
||||||
| `run.status.changed` | Run 状态 |
|
|
||||||
| `node.status.changed` / `node.progress` | 节点执行 |
|
|
||||||
| `command.confirmation.required` | 需用户确认的干预 |
|
|
||||||
| `report.version.created` | 报告版本落盘 |
|
|
||||||
| `heartbeat` | 保活 |
|
|
||||||
|
|
||||||
### 5.5 阶段 4:运行中干预
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/report-collaboration/runs/{runId}/commands
|
|
||||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/confirm
|
|
||||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/cancel
|
|
||||||
POST /api/report-collaboration/runs/{runId}/cancel
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run 活跃时 `send()` 自动走 `createCommand`(而非 `sendMessage`)。
|
|
||||||
- 可从节点抽屉 / `AgentStageSelector` 传入 `target_node_id`、`agent_run_id`。
|
|
||||||
- `InterventionCard` 展示 `confirmation_required` 命令;确认后调 `confirmCommand`。
|
|
||||||
|
|
||||||
`CommandIntent` 枚举(后端分类,前端展示 reason/impact):`answer_question`、`update_requirements`、`research_again`、`rewrite_section`、`rerun_node`、`replan`、`cancel` 等。
|
|
||||||
|
|
||||||
### 5.6 阶段 5:报告版本与改写
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/report-collaboration/sessions/{sessionId}/reports
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites/{rewriteId}/apply
|
|
||||||
POST /api/report-collaboration/sessions/{sessionId}/report-versions/{versionId}/restore
|
|
||||||
```
|
|
||||||
|
|
||||||
- `createRewrite` 只生成候选,不直接覆盖正式版本。
|
|
||||||
- `applyRewrite` / `restoreReportVersion` 产生新的 head 版本,历史保留。
|
|
||||||
- `ReportPreviewDialog` 展示 `ReportVersion.markdown`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 前端状态机(接入方须知)
|
|
||||||
|
|
||||||
**不要**在页面里用零散 REST 字段拼 UI 状态。统一路径:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SessionSnapshot → applySessionSnapshot()
|
|
||||||
CollaborationEvent[] → normalizeCollaborationEvent() → applyCollaborationEvents()
|
|
||||||
→ selectors(derivePagePhase、selectPlanForCanvas、…)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.1 核心 Selector
|
|
||||||
|
|
||||||
| Selector | 用途 |
|
|
||||||
|----------|------|
|
|
||||||
| `derivePagePhase` | Composer 占位、方案卡、空态 |
|
|
||||||
| `selectPlanForCanvas` | 画布 DAG(含预览方案) |
|
|
||||||
| `selectAgentTabs` | 并行成员 Tab |
|
|
||||||
| `selectNodeStatusMap` | 节点颜色 / 状态 |
|
|
||||||
| `selectPendingConfirmationCommands` | 干预确认卡 |
|
|
||||||
| `selectHeadReportVersion` | 报告预览入口 |
|
|
||||||
| `selectStreamingAgentRunIds` | 流式成员指示 |
|
|
||||||
|
|
||||||
### 6.2 SSE 连接策略(`useReportCollaborationStream`)
|
|
||||||
|
|
||||||
- Run 非终态时连接;`completed` / `failed` / `cancelled` 后关流并 REST 最终补拉。
|
|
||||||
- 断线:指数退避重连(1s~30s),`after_seq` 取 reducer 内 `lastSeq`。
|
|
||||||
- `hasSeqGap`:暂停 live 应用,先 `listRunEvents` 回填。
|
|
||||||
|
|
||||||
### 6.3 乐观消息
|
|
||||||
|
|
||||||
用户发送时插入 `metadata.optimistic: true` 的 pending 消息;服务端镜像带相同 `idempotency_key` 时 reducer 原地替换。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 消息与 MessageList 复用
|
|
||||||
|
|
||||||
`useReportCollaborationMessages` 将 `CollaborationMessage[]` 转为 `@langchain/langgraph-sdk` 的 `Message[]`,并构造合成 `BaseStream` 供 `MessageList` 消费。
|
|
||||||
|
|
||||||
| 来源 | 渲染 |
|
|
||||||
|------|------|
|
|
||||||
| `human` / `ai` 文本 | 普通气泡 |
|
|
||||||
| `tool_calls` + `tool` | 现有工具卡 |
|
|
||||||
| `team_message` 事件 | 带 `name` 的成员发言 |
|
|
||||||
| 业务卡 | `businessCardSlot`(方案组、命令确认、报告可用) |
|
|
||||||
|
|
||||||
分组键:`core/messages/utils.ts` 中 `assistant:task-report-import` 同级扩展 `assistant:business-card`,判据 `additional_kwargs.report_collaboration_card`。
|
|
||||||
|
|
||||||
### 7.1 在其他页面嵌入 RC(最小集)
|
|
||||||
|
|
||||||
1. `useReportCollaborationSession(sessionId)`
|
|
||||||
2. `ThreadContext.Provider` + `MessageList` + `businessCardSlot`
|
|
||||||
3. 引入 `report-collaboration/styles/report-collaboration.css`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 单测
|
|
||||||
|
|
||||||
Mock 驱动(原 `api/mock.ts`)与 `deerflow.rc-mock` / `VITE_RC_MOCK` 开关已移除,前端只请求真实后端;本地演示请直接联调后端。
|
|
||||||
|
|
||||||
单测:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend-web
|
|
||||||
pnpm test:report-collaboration
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 联调验收清单
|
|
||||||
|
|
||||||
### 9.1 准备
|
|
||||||
|
|
||||||
- [ ] `report_collaboration.enabled: true`
|
|
||||||
- [ ] `report_collaboration.worker_enabled: true`
|
|
||||||
- [ ] 五个 `*_model` 已配置且可用
|
|
||||||
- [ ] Gateway 已重启(MySQL 表/列就绪)
|
|
||||||
- [ ] 前端已登录(JWT 有效)
|
|
||||||
|
|
||||||
### 9.2 探活
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s -H "Authorization: Bearer <token>" \
|
|
||||||
http://127.0.0.1:8001/api/report-collaboration/sessions
|
|
||||||
```
|
|
||||||
|
|
||||||
期望:**200** + `{ "sessions": [...] }`,而非 503。
|
|
||||||
|
|
||||||
### 9.3 页面流程(浏览器 Network)
|
|
||||||
|
|
||||||
1. 打开 `/#/page/workspace/report-collaboration`
|
|
||||||
2. 输入需求 → `POST .../sessions` **200**
|
|
||||||
3. 跳转 → `GET .../sessions/{id}` **200**,响应含 `SessionSnapshot`
|
|
||||||
4. 首条消息 → `POST .../messages` **200**
|
|
||||||
5. 请求方案 → `POST .../plan-requests` **200**
|
|
||||||
6. 采用方案 → `POST .../plans/{id}/select` **200**
|
|
||||||
7. 开始协作 → `POST .../runs` **200**
|
|
||||||
8. `GET .../runs/{id}/stream` 持续收到 SSE(`message.delta`、`node.status.changed` 等)
|
|
||||||
9. 刷新页面 → 消息与 `last_seq` 续接正确
|
|
||||||
10. 运行中输入「重新检索」→ `POST .../commands` → 确认卡 → `confirm` **200**
|
|
||||||
|
|
||||||
### 9.4 常见失败
|
|
||||||
|
|
||||||
| 现象 | 原因 | 处理 |
|
|
||||||
|------|------|------|
|
|
||||||
| 全部 503 | `enabled: false` | 改配置并重启 |
|
|
||||||
| run 一直 queued | `worker_enabled: false` | 改为 true |
|
|
||||||
| 409 选方案 | revision 过期 | 刷新后重选(前端已自动 refresh) |
|
|
||||||
| 429 | 预算/并发上限 | 调大配置或取消其他 run |
|
|
||||||
| 500 + SQL | 表缺列 | 重启 Gateway,查列同步日志 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 契约与类型维护
|
|
||||||
|
|
||||||
- **单一事实源:** `frontend-web/src/report-collaboration/api/types.ts`
|
|
||||||
- **后端镜像:** `offline-backend-20260512/backend/app/report_collaboration/contracts/`
|
|
||||||
- 修改字段须同步两份实施计划与本指南,并跑前后端相关测试。
|
|
||||||
|
|
||||||
`SessionSnapshot` 形状(摘要):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface SessionSnapshot {
|
|
||||||
session: ReportSession;
|
|
||||||
messages: CollaborationMessage[];
|
|
||||||
plans: ReportPlanCandidate[];
|
|
||||||
run: ReportRun | null;
|
|
||||||
nodes: CollaborationNodeState[];
|
|
||||||
agent_runs: AgentRun[];
|
|
||||||
commands: CollaborationCommand[];
|
|
||||||
report_versions: ReportVersion[];
|
|
||||||
last_seq: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`ReportRun` 可选字段(RC-BE-016):`usage`、`budget_exhausted_reason`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 后续工作(非对接阻塞)
|
|
||||||
|
|
||||||
| 项 | 说明 |
|
|
||||||
|----|------|
|
|
||||||
| RC-BE-019 | 灰度发布与离线部署文档 |
|
|
||||||
| 侧边栏入口 | 已放开(`workspace-nav-chat-list.tsx` 的「报告协作」项) |
|
|
||||||
| iframe 嵌入 | RC 页尚未专门适配 `embed=1`,可参考现有深链模式扩展 |
|
|
||||||
| 真人盲评 | RC-BE-018 为确定性评测集,上线前仍需计划 §11.4 人工评审 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 相关代码索引
|
|
||||||
|
|
||||||
| 用途 | 路径 |
|
|
||||||
|------|------|
|
|
||||||
| API 客户端 | `frontend-web/src/report-collaboration/api/` |
|
|
||||||
| 会话 Hook | `frontend-web/src/report-collaboration/hooks/useReportCollaborationSession.ts` |
|
|
||||||
| 页面 | `frontend-web/src/report-collaboration/pages/ReportCollaborationPage.tsx` |
|
|
||||||
| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
|
|
||||||
| 后端路由 | `offline-backend-20260512/backend/app/gateway/routers/report_collaboration.py` |
|
|
||||||
| 配置 | `offline-backend-20260512/backend/config.yaml` → `report_collaboration` |
|
|
||||||
| 持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/report_collaboration/` |
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
# OpenVibeCoding 内网可视化应用生成平台:分步开发方案
|
|
||||||
|
|
||||||
> 文档状态:待实施
|
|
||||||
>
|
|
||||||
> 目标环境:现有 Ubuntu 22.04 Docker 服务器;对外可用端口限定为 `8020-8030`。
|
|
||||||
>
|
|
||||||
> 本文基于 2026-08-31 的只读巡检编写。未在目标服务器上安装、停止或修改任何服务。
|
|
||||||
|
|
||||||
## 1. 目标与范围
|
|
||||||
|
|
||||||
将 OpenVibeCoding 改造成一个使用公司内网模型、在公司服务器 Docker 沙箱中生成并运行可视化应用的工作台。用户通过对话生成项目,查看并修改文件,在浏览器中预览运行结果。
|
|
||||||
|
|
||||||
第一期只服务以下项目类型:
|
|
||||||
|
|
||||||
- 数据大屏、数据看板、海报和展示型页面;
|
|
||||||
- React + Vite + TypeScript 前端;
|
|
||||||
- ECharts 图表、流程图;
|
|
||||||
- 可选的轻量 Python FastAPI 接口;
|
|
||||||
- 项目文件浏览、编辑、运行日志和实时预览。
|
|
||||||
|
|
||||||
第一期明确不做:
|
|
||||||
|
|
||||||
- 腾讯云 CloudBase、SCF、TCR、CodeBuddy、云端一键部署;
|
|
||||||
- 任意语言、任意 npm/PyPI 包的无限制安装;
|
|
||||||
- 用户自定义 Dockerfile、Docker Socket 挂载、宿主机命令执行;
|
|
||||||
- 直接将沙箱暴露为独立公网端口;
|
|
||||||
- 对不可信代码提供“绝对安全”的隔离承诺。
|
|
||||||
|
|
||||||
## 2. 已确认的服务器基线
|
|
||||||
|
|
||||||
目标服务器当前具备:
|
|
||||||
|
|
||||||
| 项目 | 巡检结果 | 判断 |
|
|
||||||
|---|---:|---|
|
|
||||||
| 操作系统 | Ubuntu 22.04.5 LTS | 支持 Docker 方案 |
|
|
||||||
| CPU | 32 vCPU | 足够第一期 |
|
|
||||||
| 内存 | 125 GiB;巡检时可用约 106 GiB | 足够第一期 |
|
|
||||||
| 根磁盘 | 2 TB;巡检时可用约 1.6 TB | 足够 Harbor、镜像与工作区 |
|
|
||||||
| Docker | 29.1.3,Docker Compose 2.40.3 | 已满足 |
|
|
||||||
| 负载 | 约 0.5 | 当前有余量 |
|
|
||||||
| `8020-8030` | 巡检时无监听、无 Docker 端口映射 | 可规划使用 |
|
|
||||||
|
|
||||||
该服务器已承载 DeerFlow、Coze、LiteLLM、数据库和其他容器;`80`、`8000`、`8005`、`8006`、`3306`、`5432` 等端口已经被使用。因此,本项目不能占用现有端口、网络、数据库或容器名称。
|
|
||||||
|
|
||||||
该服务器有公网 IP,且已有多项服务绑定在 `0.0.0.0`。本方案将它视作“公网可达的共享主机”处理;云安全组和宿主机防火墙必须额外核验,不能仅凭端口监听结果假设安全。
|
|
||||||
|
|
||||||
## 3. 总体架构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
U[用户浏览器] -->|:8020| G[App Builder Gateway]
|
|
||||||
U -->|:8022 /preview/<sandboxId>/| P[Preview Router]
|
|
||||||
|
|
||||||
G --> O[OpenVibe Web / Agent Server]
|
|
||||||
O --> M[现有内网模型网关]
|
|
||||||
O --> DB[(独立 PostgreSQL)]
|
|
||||||
O --> R[(独立 Redis)]
|
|
||||||
O --> SM[Local Sandbox Manager]
|
|
||||||
|
|
||||||
SM --> D[Docker Engine]
|
|
||||||
D --> S1[visual-ui Sandbox]
|
|
||||||
D --> S2[visual-fullstack Sandbox]
|
|
||||||
P --> S1
|
|
||||||
P --> S2
|
|
||||||
|
|
||||||
D --> H[Harbor :8023]
|
|
||||||
S1 --> N[内网 pnpm/npm 源或预置离线依赖]
|
|
||||||
S2 --> N
|
|
||||||
```
|
|
||||||
|
|
||||||
设计原则:
|
|
||||||
|
|
||||||
1. 保留 OpenVibe 的对话、任务、文件树、工具卡片和 OpenCode ACP 代码代理体验。
|
|
||||||
2. 模型请求仅由 OpenVibe 服务端发往既有内网模型网关;API Key 不进入沙箱。
|
|
||||||
3. 每个项目运行于独立 Docker 容器和独立工作区;沙箱只运行项目代码,不承担平台控制逻辑。
|
|
||||||
4. 浏览器预览只经过统一预览路由,不为每个项目映射新的公网端口。
|
|
||||||
5. 先用固定模板和固定依赖清单保证稳定,后续再按白名单增加依赖。
|
|
||||||
|
|
||||||
## 4. 端口规划
|
|
||||||
|
|
||||||
端口虽然可用,但应尽量少暴露。除 8020、8022 和按需开放的 8023 外,其余服务只加入 Docker 内部网络。
|
|
||||||
|
|
||||||
| 端口 | 服务 | 绑定范围 | 用途 |
|
|
||||||
|---:|---|---|---|
|
|
||||||
| 8020 | `appbuilder-gateway` | 公网访问面或 VPN 白名单 | 控制台页面、`/api`、SSE/WebSocket |
|
|
||||||
| 8021 | 预留 | 仅 `127.0.0.1` | 平台调试;第一期不对外开放 |
|
|
||||||
| 8022 | `preview-router` | 公网访问面或 VPN 白名单 | 统一预览入口:`/preview/<sandboxId>/` |
|
|
||||||
| 8023 | Harbor | 仅沙箱节点、构建节点和管理员网段 | 私有镜像仓库 HTTPS Registry |
|
|
||||||
| 8024 | 预留 | 不对外开放 | 后续 npm 私服或运维服务 |
|
|
||||||
| 8025-8030 | 预留 | 不对外开放 | 扩展、迁移或故障切换 |
|
|
||||||
|
|
||||||
### 4.1 预览路由规则
|
|
||||||
|
|
||||||
暂时没有可用泛域名时,使用路径型预览:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http(s)://<服务器地址>:8022/preview/<sandboxId>/
|
|
||||||
```
|
|
||||||
|
|
||||||
项目模板的 Vite `base` 和 HMR 路径必须由启动脚本注入为该前缀,确保图片、JS/CSS 资源及热更新不会错误请求到根路径。后续若获得 `*.preview.company.local` 内网泛域名,可切换为子域名预览,用户体验更好。
|
|
||||||
|
|
||||||
## 5. 镜像、模板与离线依赖策略
|
|
||||||
|
|
||||||
### 5.1 标准镜像
|
|
||||||
|
|
||||||
第一期维护两种版本化镜像,不允许用户自行构建 Dockerfile:
|
|
||||||
|
|
||||||
| 镜像 | 用途 | 预置内容 |
|
|
||||||
|---|---|---|
|
|
||||||
| `appbuilder/visual-ui:<version>` | 大屏、海报、纯前端 | Node.js 20、pnpm、React、Vite、TypeScript、ECharts、流程图库、统一 UI 库、Tailwind、图标与中文字体 |
|
|
||||||
| `appbuilder/visual-fullstack:<version>` | 前端加接口 | 上述前端依赖 + Python 3.12、FastAPI、Uvicorn、Pydantic |
|
|
||||||
|
|
||||||
流程图库第一期统一选择一种(建议 React Flow),避免同时生成多种流程图语法和样式。地图、背景图、字体等资源也应作为受版本控制的本地资产打包,不能依赖 Google Fonts 或公网 CDN。
|
|
||||||
|
|
||||||
### 5.2 项目模板
|
|
||||||
|
|
||||||
每次创建项目时从镜像中的模板初始化到 `/workspace`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
templates/
|
|
||||||
visual-screen/ # React + ECharts + React Flow + Mock 数据
|
|
||||||
visual-screen-api/ # visual-screen + FastAPI + /api 反向代理
|
|
||||||
poster-page/ # 海报、营销展示、静态动效
|
|
||||||
```
|
|
||||||
|
|
||||||
`visual-screen-api` 约定:前端只请求相对路径 `/api/*`;预览网关将其转发到同一沙箱的 FastAPI `8000` 端口。没有后端时保留 Mock 数据模块,生成的页面仍可运行。
|
|
||||||
|
|
||||||
### 5.3 依赖预置方式
|
|
||||||
|
|
||||||
不能仅把安装命令写进 Dockerfile 后在用户创建项目时再联网执行。正确做法是:
|
|
||||||
|
|
||||||
1. 在受控构建环境中锁定 `package.json`、`pnpm-lock.yaml`、`requirements.lock`。
|
|
||||||
2. 构建镜像时执行 `pnpm fetch`,在镜像保存只读 pnpm store;Python 包保存到 `/opt/wheelhouse`。
|
|
||||||
3. 沙箱初始化使用 `pnpm install --offline --frozen-lockfile` 和 `pip install --no-index --find-links=/opt/wheelhouse`。
|
|
||||||
4. 第一期开启“固定依赖白名单”:AI 只能使用镜像已安装的依赖;新依赖先由管理员审核、加入锁文件和下一版基础镜像。
|
|
||||||
5. 第二期部署 Verdaccio/Nexus 等内部制品源,作为受控补充而非允许访问公网 npm/PyPI。
|
|
||||||
|
|
||||||
## 6. OpenVibe 改造边界
|
|
||||||
|
|
||||||
### 6.1 保留
|
|
||||||
|
|
||||||
- 前端交互:对话、任务状态、文件浏览与修改、预览、日志;
|
|
||||||
- OpenCode ACP 代码代理;
|
|
||||||
- 项目与会话领域模型;
|
|
||||||
- 代码代理的流式事件协议。
|
|
||||||
|
|
||||||
### 6.2 替换或关闭
|
|
||||||
|
|
||||||
| OpenVibe 当前能力 | 第一期开源/内网实现 |
|
|
||||||
|---|---|
|
|
||||||
| CodeBuddy | 只启用 OpenCode,配置内网 OpenAI 兼容模型网关 |
|
|
||||||
| CloudBase、SCF 沙箱 | 新增 `LocalDockerSandboxManager` |
|
|
||||||
| TCR | Harbor |
|
|
||||||
| CloudBase 环境、用户环境池 | Docker 容器、工作区卷、沙箱记录 |
|
|
||||||
| CloudBase 消息与事件持久化 | 独立 PostgreSQL;Redis 用于临时队列与状态 |
|
|
||||||
| CloudBase 存储、函数、MCP、资源管理后台 | 第一期开关关闭或移除入口 |
|
|
||||||
| CloudBase 项目模板和系统提示词 | 替换为大屏/海报/FastAPI 模板与受控依赖提示词 |
|
|
||||||
|
|
||||||
重点改造点是将 OpenVibe 的 `scf-sandbox-manager` 和 CloudBase 生命周期调用替换为本地 Docker 生命周期。为降低前端和 OpenCode 工具改动范围,新管理器应适配现有的文件、命令、日志、健康检查与工作区信息契约,而不是改写所有 Agent 工具。
|
|
||||||
|
|
||||||
## 7. 沙箱安全基线
|
|
||||||
|
|
||||||
每个沙箱容器必须满足:
|
|
||||||
|
|
||||||
- 非 root 用户运行;禁止 `privileged`,禁止 Docker Socket,禁止宿主机目录挂载;
|
|
||||||
- `--cap-drop=ALL`、`no-new-privileges`、`pids-limit`;
|
|
||||||
- 首期默认限制:`1 vCPU`、`3 GB 内存`、`10 GB 工作区`、60 分钟空闲回收;
|
|
||||||
- 使用独立 Docker network;默认无公网出口,只允许访问预置依赖、必要的内网 DNS 和平台回调;
|
|
||||||
- 数据库、Redis、Harbor 管理接口、宿主机管理端口不在沙箱网络可访问范围;
|
|
||||||
- 运行命令、容器生命周期、文件变更和预览访问写入审计日志;
|
|
||||||
- 平台服务由受限服务账号调用 Docker API;用户代码永远不直接获得 Docker 权限。
|
|
||||||
|
|
||||||
第一期为“内部可信用户的容器隔离”。若后续允许大量非可信代码或文件上传,应迁移为 Kubernetes 节点,并评估 gVisor/Kata 等更强运行时隔离。
|
|
||||||
|
|
||||||
## 8. 分步实施计划
|
|
||||||
|
|
||||||
### 阶段 0:部署前隔离与基线确认
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. 为本项目建立独立目录、Docker Compose Project、卷和网络,命名统一为 `appbuilder-*`。
|
|
||||||
2. 确认云安全组、iptables/nftables 规则:仅允许管理员 SSH;8020、8022、8023 仅向 VPN 或指定办公出口开放。
|
|
||||||
3. 不复用当前服务器上已有的 MySQL、PostgreSQL、Redis、Nginx 配置;建立独立实例或独立命名空间。
|
|
||||||
4. 创建内部 CA 证书或确定可信 HTTPS 证书方案;Docker 构建/运行节点信任 Harbor 证书。
|
|
||||||
5. 轮换目标服务器 root 密码,改用 SSH 密钥和受限运维账号;关闭 root 密码远程登录。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- `8020-8030` 无端口冲突;
|
|
||||||
- 新容器、网络和数据卷均不影响既有 DeerFlow 等服务;
|
|
||||||
- 业务数据库和沙箱端口无法从非授权公网地址访问。
|
|
||||||
|
|
||||||
### 阶段 1:内网基础设施与镜像供应链
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. 在独立 Compose 项目中部署 Harbor,主入口为 `8023`,镜像数据使用独立持久化目录。
|
|
||||||
2. 构建并推送 `visual-ui`、`visual-fullstack` 两种基础镜像;记录镜像版本、SBOM 和依赖锁文件。
|
|
||||||
3. 完成三个模板及离线依赖初始化脚本。
|
|
||||||
4. 先不启动漏洞扫描;严格断网环境下后续通过离线包维护漏洞库。
|
|
||||||
5. 预留内部 npm/PyPI 源接入方式;第一期仅使用预置依赖。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- Docker daemon 能从 Harbor 拉取指定版本镜像;
|
|
||||||
- 在禁止公网访问的测试网络中,三个模板均能启动并构建;
|
|
||||||
- 一个 ECharts 页面、一个流程图页面和一个 FastAPI `/api/health` 均可运行。
|
|
||||||
|
|
||||||
### 阶段 2:OpenVibe 去 CloudBase 化与内网模型接入
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. Fork 固定版本的 OpenVibeCoding,建立 `local-platform` 改造分支,记录上游 commit 与许可证。
|
|
||||||
2. 只保留 OpenCode Agent;将 provider 指向现有内网模型网关,模型凭证只保存在服务端环境变量或密钥服务。
|
|
||||||
3. 将基础实体、任务、消息、流式事件的持久化迁移为独立 PostgreSQL;不再调用 CloudBase 集合。
|
|
||||||
4. 关闭 CloudBase 环境、函数、存储、数据库、资源仪表盘和相关 MCP 入口。
|
|
||||||
5. 将系统提示词和项目创建默认项改为“可视化页面 + 受控模板/依赖”。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- 不设置任何腾讯云、CloudBase、CodeBuddy、TCR 环境变量时,平台可启动;
|
|
||||||
- 用户发起“生成 ECharts 大屏”后,OpenCode 能调用内网模型并生成项目文件;
|
|
||||||
- 服务端日志和沙箱环境中均没有模型 API Key。
|
|
||||||
|
|
||||||
### 阶段 3:本地沙箱管理器与预览路由
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. 实现 `LocalDockerSandboxManager`:创建、启动、停止、超时回收、查询状态和删除容器。
|
|
||||||
2. 每个沙箱分配工作区卷、随机内部容器地址、模板类型和资源限制;在数据库记录 `sandbox_id`、项目、用户、镜像版本、过期时间和状态。
|
|
||||||
3. 实现或适配沙箱内 runner:文件读写、受限命令执行、日志流、健康检查、前端端口和 FastAPI 端口发现。
|
|
||||||
4. 实现 `preview-router`:由平台根据 `sandboxId` 维护路由,将 `/preview/<sandboxId>/` 代理到对应内部容器;支持 WebSocket/HMR。
|
|
||||||
5. 增加容器异常退出、端口未就绪、构建失败、超时与手动重启的可见状态。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- 两个用户创建的项目存在于不同容器和不同工作区;
|
|
||||||
- 修改 ECharts 配置文件后,预览地址显示修改结果;
|
|
||||||
- FastAPI 模板的 `/api/health` 可通过前端相对路径访问;
|
|
||||||
- 沙箱不能访问 Docker Socket、宿主机数据库和非白名单网络;
|
|
||||||
- 空闲超时后容器被回收,项目文件仍可恢复。
|
|
||||||
|
|
||||||
### 阶段 4:产品能力收口
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. 在文件面板支持安全的文件查看、编辑、保存、目录树刷新与下载;限制文件大小和可编辑扩展名。
|
|
||||||
2. 在对话中增加模板选择和能力提示:大屏、流程图、海报、可选 FastAPI。
|
|
||||||
3. 为 Agent 增加依赖白名单、图表主题、中文字体、Mock 数据、接口契约等明确提示词。
|
|
||||||
4. 增加“运行/停止/重启/删除沙箱”和日志面板;仅项目所有者或管理员有相应权限。
|
|
||||||
5. 增加项目快照、模板版本记录和基础审计日志。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- 从一句自然语言需求到可预览项目的完整链路可完成;
|
|
||||||
- 用户可查看并手动修改文件,刷新后改动保留;
|
|
||||||
- 页面覆盖 ECharts、流程图、响应式大屏和可选 FastAPI 接口;
|
|
||||||
- 非拥有者不能读取、修改或预览另一用户的项目。
|
|
||||||
|
|
||||||
### 阶段 5:上线前安全与运行验收
|
|
||||||
|
|
||||||
**工作项**
|
|
||||||
|
|
||||||
1. 压测 6 个并发沙箱,并记录 CPU、内存、磁盘、容器启动时间、首屏预览时间和模型等待时间。
|
|
||||||
2. 验证镜像保留策略、工作区配额、容器回收、数据库备份和 Harbor 备份恢复。
|
|
||||||
3. 执行越权、路径穿越、命令注入、网络探测、资源耗尽和模型密钥泄露测试。
|
|
||||||
4. 建立日常运维操作:新增基础依赖、构建镜像、发布模板、回滚镜像、清理工作区和审计查询。
|
|
||||||
|
|
||||||
**验收**
|
|
||||||
|
|
||||||
- 并发运行时既有 DeerFlow 等服务不受明显影响;
|
|
||||||
- 任一沙箱异常退出不会影响其他项目或平台控制服务;
|
|
||||||
- 备份可恢复项目元数据、工作区和 Harbor 镜像;
|
|
||||||
- 仅开放计划中的入口端口,内部组件无不必要公网监听。
|
|
||||||
|
|
||||||
## 9. 首期资源与并发建议
|
|
||||||
|
|
||||||
由于该主机已有多项业务,首期不应按理论最大值分配:
|
|
||||||
|
|
||||||
| 组件 | 首期限制 |
|
|
||||||
|---|---|
|
|
||||||
| Harbor | 8 vCPU、16 GB 内存、500 GB 数据盘配额 |
|
|
||||||
| OpenVibe 控制服务 | 4 vCPU、8-16 GB 内存 |
|
|
||||||
| 单个 UI 沙箱 | 1 vCPU、3 GB 内存、10 GB 工作区 |
|
|
||||||
| 单个 Fullstack 沙箱 | 1.5 vCPU、4 GB 内存、12 GB 工作区 |
|
|
||||||
| 沙箱并发 | 先限制为 6;稳定后再逐步上调 |
|
|
||||||
|
|
||||||
当并发超过 10 个,或要面向更广泛用户开放时,应优先增加独立沙箱节点,而不是继续在本共享主机上加容器。
|
|
||||||
|
|
||||||
## 10. 实施顺序与交付物
|
|
||||||
|
|
||||||
建议按以下提交/交付顺序推进,每一步都可独立验收和回退:
|
|
||||||
|
|
||||||
1. `docs: confirm appbuilder network, ports and security baseline`
|
|
||||||
2. `infra: add Harbor and versioned visual sandbox images`
|
|
||||||
3. `infra: add offline templates and dependency locks`
|
|
||||||
4. `feat: replace OpenVibe CloudBase persistence and model provider`
|
|
||||||
5. `feat: add local Docker sandbox manager`
|
|
||||||
6. `feat: add sandbox runner and path-based preview router`
|
|
||||||
7. `feat: add visual-screen and FastAPI project workflows`
|
|
||||||
8. `test: add sandbox isolation, preview and recovery coverage`
|
|
||||||
9. `ops: add backup, cleanup, monitoring and rollout runbook`
|
|
||||||
|
|
||||||
达到阶段 3 即可交付“内网模型驱动、生成并预览大屏/海报页面”的 PoC;达到阶段 5 后再作为受控的内部多人平台发布。
|
|
||||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=true
|
|||||||
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
|
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
|
||||||
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
|
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
|
||||||
|
|
||||||
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
|
|
||||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=true
|
|
||||||
|
|
||||||
# 轻应用「新建笔记」系列接口
|
# 轻应用「新建笔记」系列接口
|
||||||
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||||
# CMZS后端接口
|
# CMZS后端接口
|
||||||
|
|||||||
@@ -60,9 +60,3 @@
|
|||||||
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
||||||
# VITE_COMPACT_MENU_TYPE=A
|
# VITE_COMPACT_MENU_TYPE=A
|
||||||
# VITE_GITHUB_OAUTH_TOKEN=
|
# VITE_GITHUB_OAUTH_TOKEN=
|
||||||
|
|
||||||
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
|
|
||||||
# true 时,普通登录后进入同一 Vite 应用中隔离的 /official/*。
|
|
||||||
# 已指定 redirect 或 taskId/goPath 的登录仍保持原来的优先级。
|
|
||||||
# 仅 .env.development 默认 true;其余环境保持 false。
|
|
||||||
# VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
|
||||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=false
|
|||||||
VITE_BACKEND_BASE_URL=http://10.19.200.5:2026
|
VITE_BACKEND_BASE_URL=http://10.19.200.5:2026
|
||||||
VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api
|
VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api
|
||||||
|
|
||||||
# 内网环境保持当前定制版默认入口。
|
|
||||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
|
||||||
|
|
||||||
# 轻应用「新建笔记」系列接口
|
# 轻应用「新建笔记」系列接口
|
||||||
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||||
# CMZS后端接口
|
# CMZS后端接口
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ VITE_MOCK_SQ_REPORT=false
|
|||||||
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
|
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
|
||||||
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
|
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
|
||||||
|
|
||||||
# 线上保持当前定制版默认入口。
|
|
||||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
|
||||||
|
|
||||||
# 轻应用「新建笔记」系列接口
|
# 轻应用「新建笔记」系列接口
|
||||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||||
# CMZS后端接口
|
# CMZS后端接口
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=false
|
|||||||
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
|
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
|
||||||
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
|
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
|
||||||
|
|
||||||
# SIT 环境保持当前定制版默认入口。
|
|
||||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
|
||||||
|
|
||||||
# 轻应用「新建笔记」系列接口
|
# 轻应用「新建笔记」系列接口
|
||||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||||
# CMZS后端接口
|
# CMZS后端接口
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# 技能知识归纳与“知识梳理”助手知识库
|
|
||||||
|
|
||||||
管理员在「技能管理 → 技能知识归纳」中选择一个或多个技能、一个或多个目标,系统按“技能 × 目标”创建后台任务。界面提供总体进度、单项失败重试、实体/关系审核、绑定重归纳、自动同步和两种解绑动作。技能列表通过一次 bindings 查询展示归纳目标,避免逐技能请求。
|
|
||||||
|
|
||||||
扫描仅处理知识资料。代码、脚本、锁文件、密钥文件以及 Markdown 代码块不会进入上传、摘要、提示词或知识指纹;ZIP 只读取逐项验证通过的知识成员。自动监听只比较该知识指纹,所以纯代码改动不会产生 stale 状态。
|
|
||||||
|
|
||||||
知识库页面新增「普通知识库 / 助手知识库」并行入口。管理员可将系统默认入口设为 v1(普通 WeKnora)或 v2(全局“知识梳理”)。助手库无需 WeKnora 即可初始化,所有用户可只读查看和搜索 Wiki;管理员可上传本地文件、从普通库导入、重新导入或删除来源、查看版本并回滚。来源删除只移除对应贡献,共享实体与关系继续保留。
|
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-US" suppressHydrationWarning>
|
<html lang="en-US" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
var t = localStorage.getItem("strategy-theme");
|
var t = localStorage.getItem("strategy-theme");
|
||||||
var root = document.documentElement;
|
var root = document.documentElement;
|
||||||
root.classList.remove("dark", "light");
|
root.classList.remove("dark", "light");
|
||||||
if (t === "light") return;
|
if (t === "light") return;
|
||||||
root.classList.add("dark");
|
root.classList.add("dark");
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>CM 助手</title>
|
<title>CM 助手</title>
|
||||||
<script src="/runtime-config.js" data-runtime-config></script>
|
<script src="/runtime-config.js" data-runtime-config></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
|
||||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
||||||
},
|
},
|
||||||
|
|||||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-labelledby="title">
|
||||||
|
<title id="title">CM 助手</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoTopLeft" x1="7" y1="7" x2="25" y2="20" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#5DB5FF"/>
|
||||||
|
<stop offset="1" stop-color="#328AF2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoTopRight" x1="24" y1="3" x2="43" y2="19" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#62B2FF"/>
|
||||||
|
<stop offset="1" stop-color="#4B98F4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoLeft" x1="5" y1="14" x2="24" y2="45" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2C7CF0"/>
|
||||||
|
<stop offset="1" stop-color="#1F5EDA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoBottom" x1="9" y1="31" x2="43" y2="40" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#276DE3"/>
|
||||||
|
<stop offset="1" stop-color="#4E9DF5"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- The mark is drawn as four independent folded faces. Together they form
|
||||||
|
the open C-shaped cube used in the product reference. -->
|
||||||
|
<path d="M5 13.5 24 3v10L14 18.5 5 13.5Z" fill="url(#logoTopLeft)"/>
|
||||||
|
<path d="M24 3 43 13.5 34 18.5 24 13V3Z" fill="url(#logoTopRight)"/>
|
||||||
|
<path d="M5 13.5 14 18.5v11L24 35v10L5 34.5v-21Z" fill="url(#logoLeft)"/>
|
||||||
|
<path d="M14 29.5 24 35l10-5.5 9 5L24 45 5 34.5l9-5Z" fill="url(#logoBottom)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -45,7 +45,6 @@ import {
|
|||||||
} from "@/position-roundtable/demo/position-roundtable-demo-config";
|
} from "@/position-roundtable/demo/position-roundtable-demo-config";
|
||||||
import { ChatRuntime } from "./pages/WorkspaceRoutes";
|
import { ChatRuntime } from "./pages/WorkspaceRoutes";
|
||||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||||
import OfficialDeerFlowEntry from "./pages/OfficialDeerFlowEntry";
|
|
||||||
|
|
||||||
const PUBLIC_EMBED_PREFIXES = [
|
const PUBLIC_EMBED_PREFIXES = [
|
||||||
"/share/",
|
"/share/",
|
||||||
@@ -204,7 +203,6 @@ export function App() {
|
|||||||
path="/roundtable-dashboard"
|
path="/roundtable-dashboard"
|
||||||
element={<DashboardStandalonePage />}
|
element={<DashboardStandalonePage />}
|
||||||
/>
|
/>
|
||||||
<Route path="/official/*" element={<OfficialDeerFlowEntry />} />
|
|
||||||
<Route path="/page/*" element={pageElement} />
|
<Route path="/page/*" element={pageElement} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2064"
|
|
||||||
height="2591" viewBox="0 0 2064 2591">
|
|
||||||
<path fill-rule="nonzero"
|
|
||||||
d="M 68.582031 0 C 66.890625 0 65.300781 1 64.617188 2.640625 C 49.46875 38.800781 33.71875 76.199219 33.628906 106.539062 C 33.550781 134.230469 46.445312 155.730469 59.949219 175.71875 L 59.949219 175.730469 C 52.414062 175.671875 44.992188 175.371094 37.472656 174.171875 C 27.210938 172.53125 16.585938 169.199219 5.585938 165.738281 C 5.15625 165.609375 4.722656 165.539062 4.296875 165.539062 C 2.824219 165.539062 1.417969 166.300781 0.625 167.609375 C -0.402344 169.300781 -0.140625 171.46875 1.257812 172.871094 L 1.261719 172.878906 C 11.230469 182.871094 21.8125 193.53125 36.980469 200.050781 C 51.5625 206.320312 70.167969 208.628906 88.085938 210.78125 L 88.027344 210.78125 C 127.152344 253.410156 166.617188 296.300781 215.761719 334.941406 C 230.558594 346.578125 246.292969 357.878906 261.785156 369 C 277.171875 380.050781 292.441406 391.011719 306.660156 402.191406 C 326.132812 417.511719 343.5625 433.179688 356.773438 450.019531 C 377.808594 476.828125 388.449219 506.960938 395.4375 538.628906 L 395.4375 538.640625 C 372.683594 510.710938 349.328125 483.929688 324.855469 469.339844 C 295.957031 452.121094 265.800781 452 236.960938 451.808594 L 236.933594 451.820312 C 235.792969 451.808594 234.707031 452.261719 233.898438 453.070312 C 233.085938 453.871094 232.632812 454.96875 232.636719 456.121094 C 232.722656 494.011719 232.699219 533.539062 256.304688 567.480469 C 279.351562 600.621094 324.738281 628.019531 369.191406 654.679688 L 369.132812 654.691406 C 371.476562 683.539062 373.40625 710.691406 367.109375 728.03125 C 365.828125 731.558594 364.191406 734.71875 362.058594 738.820312 C 358.910156 744.871094 354.964844 752.441406 350.417969 764.960938 C 342.890625 785.699219 333.769531 819.839844 323.648438 883.109375 C 303.347656 1010.019531 278.722656 1255.949219 308.882812 1409.269531 C 326.265625 1497.660156 361.828125 1555.210938 393.511719 1606.519531 C 417.042969 1644.632812 438.207031 1678.960938 448.835938 1720.09375 C 466.921875 1790.089844 454.648438 1880.246094 444.5 1954.386719 C 440.613281 1982.78125 437.042969 2008.839844 435.726562 2030.261719 C 433.410156 2067.929688 438.066406 2091.179688 442.304688 2112.347656 L 442.304688 2112.355469 C 445.910156 2130.347656 449.152344 2146.589844 447.757812 2169.28125 C 447.429688 2174.613281 446.835938 2180.359375 445.933594 2186.535156 L 445.933594 2186.542969 C 436.582031 2250.328125 393.613281 2362.808594 362.105469 2416.636719 C 339.761719 2454.804688 323.546875 2462.851562 309.597656 2469.925781 C 303.980469 2472.777344 298.261719 2475.660156 292.691406 2480.238281 C 273.378906 2496.113281 256.457031 2532.078125 240.183594 2566.417969 C 239.578125 2567.703125 239.640625 2569.203125 240.363281 2570.429688 C 241.082031 2571.65625 242.359375 2572.449219 243.777344 2572.542969 L 243.789062 2572.542969 L 243.882812 2572.550781 L 243.882812 2572.554688 C 261.582031 2573.746094 279.460938 2574.957031 296.386719 2574.957031 C 323.671875 2574.957031 349.332031 2571.84375 368.355469 2559.785156 C 392.640625 2544.394531 405.109375 2514.960938 414.488281 2493.074219 C 417.011719 2487.183594 419.289062 2481.871094 421.546875 2477.308594 C 432.054688 2456.039062 441.753906 2451.664062 451.855469 2446.960938 C 453.054688 2446.402344 453.929688 2445.320312 454.226562 2444.042969 L 454.238281 2443.996094 L 454.238281 2443.988281 C 476.210938 2348.800781 498.035156 2254.347656 524.300781 2196.363281 L 524.300781 2196.367188 C 536.363281 2169.730469 549.363281 2150.796875 562.695312 2131.402344 C 576.28125 2111.625 590.1875 2091.402344 603.300781 2062.445312 L 603.304688 2062.453125 C 605.183594 2058.292969 607.054688 2053.949219 608.894531 2049.421875 C 625.066406 2009.675781 639.746094 1954.289062 653.585938 1902.101562 C 663.0625 1866.367188 672.15625 1832.09375 681.085938 1805.085938 C 699.308594 1749.992188 716.863281 1725.09375 734.371094 1705.144531 L 734.421875 1705.148438 C 727.285156 1740.746094 721.597656 1776.628906 729.320312 1807.472656 C 733.351562 1823.570312 740.996094 1838.160156 749.066406 1853.613281 C 758.828125 1872.296875 769.433594 1892.636719 776.359375 1919.675781 C 783.289062 1946.71875 786.574219 1980.605469 789.527344 2011.140625 L 789.527344 2011.148438 C 791.875 2035.402344 794.023438 2057.78125 797.746094 2072.316406 L 797.746094 2072.320312 C 797.929688 2073.023438 798.128906 2073.769531 798.316406 2074.441406 L 798.320312 2074.441406 C 802.132812 2087.941406 807.617188 2093.914062 813.515625 2100.324219 C 821.132812 2108.597656 830.351562 2118.441406 839.742188 2151.671875 L 839.738281 2151.675781 C 840.105469 2152.96875 840.402344 2154.039062 840.765625 2155.398438 C 857.484375 2217.601562 874.097656 2354.589844 872.078125 2423.390625 C 870.472656 2478.15625 857.140625 2488.390625 845.339844 2498.136719 C 842.320312 2500.632812 839.085938 2503.296875 836.09375 2506.566406 C 820.847656 2523.230469 812.652344 2554.886719 804.800781 2584.753906 C 804.457031 2586.0625 804.75 2587.453125 805.589844 2588.515625 C 806.40625 Line truncated
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -1,158 +1,158 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { type LucideIcon, XIcon } from "lucide-react";
|
import { type LucideIcon, XIcon } from "lucide-react";
|
||||||
import { forwardRef, type ComponentProps, type HTMLAttributes } from "react";
|
import { forwardRef, type ComponentProps, type HTMLAttributes } from "react";
|
||||||
|
|
||||||
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col overflow-hidden rounded-[1.35rem] border border-border bg-background shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3",
|
"flex flex-col overflow-hidden rounded-[1.35rem] border border-border bg-background shadow-[0_24px_60px_-28px_rgba(15,23,42,0.16)] ring-1 ring-black/3",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
|
export type ArtifactHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const ArtifactHeader = ({
|
export const ArtifactHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtifactHeaderProps) => (
|
}: ArtifactHeaderProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-between border-b border-border bg-background px-4 py-3",
|
"flex items-center justify-between border-b border-border bg-background px-4 py-3",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactCloseProps = ComponentProps<typeof Button>;
|
export type ArtifactCloseProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const ArtifactClose = ({
|
export const ArtifactClose = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
...props
|
...props
|
||||||
}: ArtifactCloseProps) => (
|
}: ArtifactCloseProps) => (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground hover:text-foreground size-8 p-0",
|
"text-muted-foreground hover:text-foreground size-8 p-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <XIcon className="size-4" />}
|
{children ?? <XIcon className="size-4" />}
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground text-sm font-semibold tracking-tight",
|
"text-foreground text-sm font-semibold tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
export const ArtifactDescription = ({
|
export const ArtifactDescription = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtifactDescriptionProps) => (
|
}: ArtifactDescriptionProps) => (
|
||||||
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
|
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
|
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const ArtifactActions = ({
|
export const ArtifactActions = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtifactActionsProps) => (
|
}: ArtifactActionsProps) => (
|
||||||
<div className={cn("flex items-center gap-1.5", className)} {...props} />
|
<div className={cn("flex items-center gap-1.5", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArtifactAction = ({
|
export const ArtifactAction = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
label,
|
label,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
...props
|
...props
|
||||||
}: ArtifactActionProps) => {
|
}: ArtifactActionProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground hover:text-foreground hover:bg-accent/70 size-8 rounded-full p-0 transition-all duration-200 hover:scale-[1.03]",
|
"text-muted-foreground hover:text-foreground hover:bg-accent/70 size-8 rounded-full p-0 transition-all duration-200 hover:scale-[1.03]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon className="size-4" /> : children}
|
{Icon ? <Icon className="size-4" /> : children}
|
||||||
<span className="sr-only">{label || tooltip}</span>
|
<span className="sr-only">{label || tooltip}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{tooltip}</p>
|
<p>{tooltip}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
|
export type ArtifactContentProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>(
|
export const ArtifactContent = forwardRef<HTMLDivElement, ArtifactContentProps>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-0 flex-1 overflow-auto bg-background p-4",
|
"min-h-0 flex-1 overflow-auto bg-background p-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ArtifactContent.displayName = "ArtifactContent";
|
ArtifactContent.displayName = "ArtifactContent";
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
|
import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
type CanvasProps = ReactFlowProps & {
|
type CanvasProps = ReactFlowProps & {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Canvas = ({ children, ...props }: CanvasProps) => (
|
export const Canvas = ({ children, ...props }: CanvasProps) => (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
deleteKeyCode={["Backspace", "Delete"]}
|
deleteKeyCode={["Backspace", "Delete"]}
|
||||||
fitView
|
fitView
|
||||||
panOnDrag={false}
|
panOnDrag={false}
|
||||||
panOnScroll
|
panOnScroll
|
||||||
selectionOnDrag={true}
|
selectionOnDrag={true}
|
||||||
zoomOnDoubleClick={false}
|
zoomOnDoubleClick={false}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Background bgColor="var(--sidebar)" />
|
<Background bgColor="var(--sidebar)" />
|
||||||
{children}
|
{children}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
@@ -1,239 +1,239 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
BrainIcon,
|
BrainIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
DotIcon,
|
DotIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
memo,
|
memo,
|
||||||
useContext,
|
useContext,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
type ChainOfThoughtContextValue = {
|
type ChainOfThoughtContextValue = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: (open: boolean) => void;
|
setIsOpen: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
|
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const useChainOfThought = () => {
|
const useChainOfThought = () => {
|
||||||
const context = useContext(ChainOfThoughtContext);
|
const context = useContext(ChainOfThoughtContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"ChainOfThought components must be used within ChainOfThought",
|
"ChainOfThought components must be used within ChainOfThought",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChainOfThoughtProps = ComponentProps<"div"> & {
|
export type ChainOfThoughtProps = ComponentProps<"div"> & {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChainOfThought = memo(
|
export const ChainOfThought = memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
open,
|
open,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ChainOfThoughtProps) => {
|
}: ChainOfThoughtProps) => {
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
const [isOpen, setIsOpen] = useControllableState({
|
||||||
prop: open,
|
prop: open,
|
||||||
defaultProp: defaultOpen,
|
defaultProp: defaultOpen,
|
||||||
onChange: onOpenChange,
|
onChange: onOpenChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
const chainOfThoughtContext = useMemo(
|
const chainOfThoughtContext = useMemo(
|
||||||
() => ({ isOpen, setIsOpen }),
|
() => ({ isOpen, setIsOpen }),
|
||||||
[isOpen, setIsOpen],
|
[isOpen, setIsOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
|
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
|
||||||
<div className={cn("not-prose", className)} {...props}>
|
<div className={cn("not-prose", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ChainOfThoughtContext.Provider>
|
</ChainOfThoughtContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtHeaderProps = ComponentProps<
|
export type ChainOfThoughtHeaderProps = ComponentProps<
|
||||||
typeof CollapsibleTrigger
|
typeof CollapsibleTrigger
|
||||||
> & {
|
> & {
|
||||||
icon?: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChainOfThoughtHeader = memo(
|
export const ChainOfThoughtHeader = memo(
|
||||||
({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => {
|
({ className, children, icon, ...props }: ChainOfThoughtHeaderProps) => {
|
||||||
const { isOpen, setIsOpen } = useChainOfThought();
|
const { isOpen, setIsOpen } = useChainOfThought();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
|
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon ?? <BrainIcon className="size-4" />}
|
{icon ?? <BrainIcon className="size-4" />}
|
||||||
<span className="flex-1 text-left">
|
<span className="flex-1 text-left">
|
||||||
{children ?? "Chain of Thought"}
|
{children ?? "Chain of Thought"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0",
|
isOpen ? "rotate-180" : "rotate-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
||||||
icon?: LucideIcon | React.ReactElement;
|
icon?: LucideIcon | React.ReactElement;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
status?: "complete" | "active" | "pending";
|
status?: "complete" | "active" | "pending";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChainOfThoughtStep = memo(
|
export const ChainOfThoughtStep = memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
icon: Icon = DotIcon,
|
icon: Icon = DotIcon,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
status = "complete",
|
status = "complete",
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ChainOfThoughtStepProps) => {
|
}: ChainOfThoughtStepProps) => {
|
||||||
const statusStyles = {
|
const statusStyles = {
|
||||||
complete: "text-muted-foreground",
|
complete: "text-muted-foreground",
|
||||||
active: "text-foreground",
|
active: "text-foreground",
|
||||||
pending: "text-muted-foreground/50",
|
pending: "text-muted-foreground/50",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-2 text-sm",
|
"flex gap-2 text-sm",
|
||||||
statusStyles[status],
|
statusStyles[status],
|
||||||
"fade-in-0 slide-in-from-top-2 animate-in",
|
"fade-in-0 slide-in-from-top-2 animate-in",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative mt-0.5">
|
<div className="relative mt-0.5">
|
||||||
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
|
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
|
||||||
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
|
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-2 overflow-hidden">
|
<div className="flex-1 space-y-2 overflow-hidden">
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="text-muted-foreground text-xs">{description}</div>
|
<div className="text-muted-foreground text-xs">{description}</div>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
|
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ChainOfThoughtSearchResults = memo(
|
export const ChainOfThoughtSearchResults = memo(
|
||||||
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
|
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap items-center gap-2 overflow-x-hidden",
|
"flex flex-wrap items-center gap-2 overflow-x-hidden",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
|
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
|
||||||
|
|
||||||
export const ChainOfThoughtSearchResult = memo(
|
export const ChainOfThoughtSearchResult = memo(
|
||||||
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
|
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
|
||||||
<Badge
|
<Badge
|
||||||
className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)}
|
className={cn("gap-1 px-2 py-0.5 text-xs font-normal", className)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtContentProps = ComponentProps<
|
export type ChainOfThoughtContentProps = ComponentProps<
|
||||||
typeof CollapsibleContent
|
typeof CollapsibleContent
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ChainOfThoughtContent = memo(
|
export const ChainOfThoughtContent = memo(
|
||||||
({ className, children, ...props }: ChainOfThoughtContentProps) => {
|
({ className, children, ...props }: ChainOfThoughtContentProps) => {
|
||||||
const { isOpen } = useChainOfThought();
|
const { isOpen } = useChainOfThought();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={isOpen}>
|
<Collapsible open={isOpen}>
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 space-y-3",
|
"mt-2 space-y-3",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
|
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
|
||||||
caption?: string;
|
caption?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChainOfThoughtImage = memo(
|
export const ChainOfThoughtImage = memo(
|
||||||
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
|
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
|
||||||
<div className={cn("mt-2 space-y-2", className)} {...props}>
|
<div className={cn("mt-2 space-y-2", className)} {...props}>
|
||||||
<div className="bg-muted relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg p-3">
|
<div className="bg-muted relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg p-3">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
|
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ChainOfThought.displayName = "ChainOfThought";
|
ChainOfThought.displayName = "ChainOfThought";
|
||||||
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
|
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
|
||||||
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
|
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
|
||||||
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
|
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
|
||||||
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
|
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
|
||||||
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
|
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
|
||||||
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
|
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
|
||||||
@@ -1,71 +1,71 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BookmarkIcon, type LucideProps } from "lucide-react";
|
import { BookmarkIcon, type LucideProps } from "lucide-react";
|
||||||
import type { ComponentProps, HTMLAttributes } from "react";
|
import type { ComponentProps, HTMLAttributes } from "react";
|
||||||
|
|
||||||
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
export type CheckpointProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const Checkpoint = ({
|
export const Checkpoint = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: CheckpointProps) => (
|
}: CheckpointProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
|
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CheckpointIconProps = LucideProps;
|
export type CheckpointIconProps = LucideProps;
|
||||||
|
|
||||||
export const CheckpointIcon = ({
|
export const CheckpointIcon = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: CheckpointIconProps) =>
|
}: CheckpointIconProps) =>
|
||||||
children ?? (
|
children ?? (
|
||||||
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
|
<BookmarkIcon className={cn("size-4 shrink-0", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
export type CheckpointTriggerProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CheckpointTrigger = ({
|
export const CheckpointTrigger = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "sm",
|
size = "sm",
|
||||||
tooltip,
|
tooltip,
|
||||||
...props
|
...props
|
||||||
}: CheckpointTriggerProps) =>
|
}: CheckpointTriggerProps) =>
|
||||||
tooltip ? (
|
tooltip ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button size={size} type="button" variant={variant} {...props}>
|
<Button size={size} type="button" variant={variant} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent align="start" side="bottom">
|
<TooltipContent align="start" side="bottom">
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Button size={size} type="button" variant={variant} {...props}>
|
<Button size={size} type="button" variant={variant} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -1,150 +1,150 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { copyTextToClipboard } from "@/core/clipboard";
|
import { copyTextToClipboard } from "@/core/clipboard";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
createContext,
|
createContext,
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
|
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
|
||||||
|
|
||||||
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
code: string;
|
code: string;
|
||||||
language: BundledLanguage;
|
language: BundledLanguage;
|
||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CodeBlockContextType = {
|
type CodeBlockContextType = {
|
||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CodeBlockContext = createContext<CodeBlockContextType>({
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
code: "",
|
code: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const lineNumberTransformer: ShikiTransformer = {
|
const lineNumberTransformer: ShikiTransformer = {
|
||||||
name: "line-numbers",
|
name: "line-numbers",
|
||||||
line(node, line) {
|
line(node, line) {
|
||||||
node.children.unshift({
|
node.children.unshift({
|
||||||
type: "element",
|
type: "element",
|
||||||
tagName: "span",
|
tagName: "span",
|
||||||
properties: {
|
properties: {
|
||||||
className: [
|
className: [
|
||||||
"inline-block",
|
"inline-block",
|
||||||
"min-w-10",
|
"min-w-10",
|
||||||
"mr-4",
|
"mr-4",
|
||||||
"text-right",
|
"text-right",
|
||||||
"select-none",
|
"select-none",
|
||||||
"text-muted-foreground",
|
"text-muted-foreground",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
children: [{ type: "text", value: String(line) }],
|
children: [{ type: "text", value: String(line) }],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function highlightCode(
|
export async function highlightCode(
|
||||||
code: string,
|
code: string,
|
||||||
language: BundledLanguage,
|
language: BundledLanguage,
|
||||||
showLineNumbers = false,
|
showLineNumbers = false,
|
||||||
) {
|
) {
|
||||||
const transformers: ShikiTransformer[] = showLineNumbers
|
const transformers: ShikiTransformer[] = showLineNumbers
|
||||||
? [lineNumberTransformer]
|
? [lineNumberTransformer]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return await Promise.all([
|
return await Promise.all([
|
||||||
codeToHtml(code, {
|
codeToHtml(code, {
|
||||||
lang: language,
|
lang: language,
|
||||||
theme: "one-light",
|
theme: "one-light",
|
||||||
transformers,
|
transformers,
|
||||||
}),
|
}),
|
||||||
codeToHtml(code, {
|
codeToHtml(code, {
|
||||||
lang: language,
|
lang: language,
|
||||||
theme: "one-dark-pro",
|
theme: "one-dark-pro",
|
||||||
transformers,
|
transformers,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlock = ({
|
export const CodeBlock = ({
|
||||||
code,
|
code,
|
||||||
language,
|
language,
|
||||||
showLineNumbers = false,
|
showLineNumbers = false,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: CodeBlockProps) => {
|
}: CodeBlockProps) => {
|
||||||
const [html, setHtml] = useState<string>("");
|
const [html, setHtml] = useState<string>("");
|
||||||
const [darkHtml, setDarkHtml] = useState<string>("");
|
const [darkHtml, setDarkHtml] = useState<string>("");
|
||||||
const mounted = useRef(false);
|
const mounted = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
|
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
|
||||||
if (!mounted.current) {
|
if (!mounted.current) {
|
||||||
setHtml(light);
|
setHtml(light);
|
||||||
setDarkHtml(dark);
|
setDarkHtml(dark);
|
||||||
mounted.current = true;
|
mounted.current = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted.current = false;
|
mounted.current = false;
|
||||||
};
|
};
|
||||||
}, [code, language, showLineNumbers]);
|
}, [code, language, showLineNumbers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeBlockContext.Provider value={{ code }}>
|
<CodeBlockContext.Provider value={{ code }}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group bg-background text-foreground relative size-full overflow-hidden rounded-md border",
|
"group bg-background text-foreground relative size-full overflow-hidden rounded-md border",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative size-full">
|
<div className="relative size-full">
|
||||||
<div
|
<div
|
||||||
className="[&>pre]:bg-background! [&>pre]:text-foreground! size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
className="[&>pre]:bg-background! [&>pre]:text-foreground! size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="[&>pre]:bg-background! [&>pre]:text-foreground! hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
className="[&>pre]:bg-background! [&>pre]:text-foreground! hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
||||||
dangerouslySetInnerHTML={{ __html: darkHtml }}
|
dangerouslySetInnerHTML={{ __html: darkHtml }}
|
||||||
/>
|
/>
|
||||||
{children && (
|
{children && (
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-2">
|
<div className="absolute top-2 right-2 flex items-center gap-2">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CodeBlockContext.Provider>
|
</CodeBlockContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
onCopy?: () => void;
|
onCopy?: () => void;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CodeBlockCopyButton = ({
|
export const CodeBlockCopyButton = ({
|
||||||
onCopy,
|
onCopy,
|
||||||
onError,
|
onError,
|
||||||
timeout = 2000,
|
timeout = 2000,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: CodeBlockCopyButtonProps) => {
|
}: CodeBlockCopyButtonProps) => {
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const { code } = useContext(CodeBlockContext);
|
const { code } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
const copyToClipboard = async () => {
|
||||||
@@ -152,23 +152,23 @@ export const CodeBlockCopyButton = ({
|
|||||||
await copyTextToClipboard(code);
|
await copyTextToClipboard(code);
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
onCopy?.();
|
onCopy?.();
|
||||||
setTimeout(() => setIsCopied(false), timeout);
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onError?.(error as Error);
|
onError?.(error as Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <Icon size={14} />}
|
{children ?? <Icon size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import type { ConnectionLineComponent } from "@xyflow/react";
|
import type { ConnectionLineComponent } from "@xyflow/react";
|
||||||
|
|
||||||
const HALF = 0.5;
|
const HALF = 0.5;
|
||||||
|
|
||||||
export const Connection: ConnectionLineComponent = ({
|
export const Connection: ConnectionLineComponent = ({
|
||||||
fromX,
|
fromX,
|
||||||
fromY,
|
fromY,
|
||||||
toX,
|
toX,
|
||||||
toY,
|
toY,
|
||||||
}) => (
|
}) => (
|
||||||
<g>
|
<g>
|
||||||
<path
|
<path
|
||||||
className="animated"
|
className="animated"
|
||||||
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
|
d={`M${fromX},${fromY} C ${fromX + (toX - fromX) * HALF},${fromY} ${fromX + (toX - fromX) * HALF},${toY} ${toX},${toY}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="var(--color-ring)"
|
stroke="var(--color-ring)"
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx={toX}
|
cx={toX}
|
||||||
cy={toY}
|
cy={toY}
|
||||||
fill="#fff"
|
fill="#fff"
|
||||||
r={3}
|
r={3}
|
||||||
stroke="var(--color-ring)"
|
stroke="var(--color-ring)"
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
@@ -1,408 +1,408 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { LanguageModelUsage } from "ai";
|
import type { LanguageModelUsage } from "ai";
|
||||||
import { type ComponentProps, createContext, useContext } from "react";
|
import { type ComponentProps, createContext, useContext } from "react";
|
||||||
import { getUsage } from "tokenlens";
|
import { getUsage } from "tokenlens";
|
||||||
|
|
||||||
const PERCENT_MAX = 100;
|
const PERCENT_MAX = 100;
|
||||||
const ICON_RADIUS = 10;
|
const ICON_RADIUS = 10;
|
||||||
const ICON_VIEWBOX = 24;
|
const ICON_VIEWBOX = 24;
|
||||||
const ICON_CENTER = 12;
|
const ICON_CENTER = 12;
|
||||||
const ICON_STROKE_WIDTH = 2;
|
const ICON_STROKE_WIDTH = 2;
|
||||||
|
|
||||||
type ModelId = string;
|
type ModelId = string;
|
||||||
|
|
||||||
type ContextSchema = {
|
type ContextSchema = {
|
||||||
usedTokens: number;
|
usedTokens: number;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
usage?: LanguageModelUsage;
|
usage?: LanguageModelUsage;
|
||||||
modelId?: ModelId;
|
modelId?: ModelId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextContext = createContext<ContextSchema | null>(null);
|
const ContextContext = createContext<ContextSchema | null>(null);
|
||||||
|
|
||||||
const useContextValue = () => {
|
const useContextValue = () => {
|
||||||
const context = useContext(ContextContext);
|
const context = useContext(ContextContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Context components must be used within Context");
|
throw new Error("Context components must be used within Context");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
|
export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
|
||||||
|
|
||||||
export const Context = ({
|
export const Context = ({
|
||||||
usedTokens,
|
usedTokens,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
usage,
|
usage,
|
||||||
modelId,
|
modelId,
|
||||||
...props
|
...props
|
||||||
}: ContextProps) => (
|
}: ContextProps) => (
|
||||||
<ContextContext.Provider
|
<ContextContext.Provider
|
||||||
value={{
|
value={{
|
||||||
usedTokens,
|
usedTokens,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
usage,
|
usage,
|
||||||
modelId,
|
modelId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
||||||
</ContextContext.Provider>
|
</ContextContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ContextIcon = () => {
|
const ContextIcon = () => {
|
||||||
const { usedTokens, maxTokens } = useContextValue();
|
const { usedTokens, maxTokens } = useContextValue();
|
||||||
const circumference = 2 * Math.PI * ICON_RADIUS;
|
const circumference = 2 * Math.PI * ICON_RADIUS;
|
||||||
const usedPercent = usedTokens / maxTokens;
|
const usedPercent = usedTokens / maxTokens;
|
||||||
const dashOffset = circumference * (1 - usedPercent);
|
const dashOffset = circumference * (1 - usedPercent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-label="Model context usage"
|
aria-label="Model context usage"
|
||||||
height="20"
|
height="20"
|
||||||
role="img"
|
role="img"
|
||||||
style={{ color: "currentcolor" }}
|
style={{ color: "currentcolor" }}
|
||||||
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
||||||
width="20"
|
width="20"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
cx={ICON_CENTER}
|
cx={ICON_CENTER}
|
||||||
cy={ICON_CENTER}
|
cy={ICON_CENTER}
|
||||||
fill="none"
|
fill="none"
|
||||||
opacity="0.25"
|
opacity="0.25"
|
||||||
r={ICON_RADIUS}
|
r={ICON_RADIUS}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx={ICON_CENTER}
|
cx={ICON_CENTER}
|
||||||
cy={ICON_CENTER}
|
cy={ICON_CENTER}
|
||||||
fill="none"
|
fill="none"
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
r={ICON_RADIUS}
|
r={ICON_RADIUS}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeDasharray={`${circumference} ${circumference}`}
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
strokeDashoffset={dashOffset}
|
strokeDashoffset={dashOffset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
|
style={{ transformOrigin: "center", transform: "rotate(-90deg)" }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextTriggerProps = ComponentProps<typeof Button>;
|
export type ContextTriggerProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
||||||
const { usedTokens, maxTokens } = useContextValue();
|
const { usedTokens, maxTokens } = useContextValue();
|
||||||
const usedPercent = usedTokens / maxTokens;
|
const usedPercent = usedTokens / maxTokens;
|
||||||
const renderedPercent = new Intl.NumberFormat("en-US", {
|
const renderedPercent = new Intl.NumberFormat("en-US", {
|
||||||
style: "percent",
|
style: "percent",
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
}).format(usedPercent);
|
}).format(usedPercent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<Button type="button" variant="ghost" {...props}>
|
<Button type="button" variant="ghost" {...props}>
|
||||||
<span className="text-muted-foreground font-medium">
|
<span className="text-muted-foreground font-medium">
|
||||||
{renderedPercent}
|
{renderedPercent}
|
||||||
</span>
|
</span>
|
||||||
<ContextIcon />
|
<ContextIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
|
export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
|
||||||
|
|
||||||
export const ContextContent = ({
|
export const ContextContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ContextContentProps) => (
|
}: ContextContentProps) => (
|
||||||
<HoverCardContent
|
<HoverCardContent
|
||||||
className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
|
className={cn("min-w-60 divide-y overflow-hidden p-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ContextContentHeaderProps = ComponentProps<"div">;
|
export type ContextContentHeaderProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextContentHeader = ({
|
export const ContextContentHeader = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ContextContentHeaderProps) => {
|
}: ContextContentHeaderProps) => {
|
||||||
const { usedTokens, maxTokens } = useContextValue();
|
const { usedTokens, maxTokens } = useContextValue();
|
||||||
const usedPercent = usedTokens / maxTokens;
|
const usedPercent = usedTokens / maxTokens;
|
||||||
const displayPct = new Intl.NumberFormat("en-US", {
|
const displayPct = new Intl.NumberFormat("en-US", {
|
||||||
style: "percent",
|
style: "percent",
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
}).format(usedPercent);
|
}).format(usedPercent);
|
||||||
const used = new Intl.NumberFormat("en-US", {
|
const used = new Intl.NumberFormat("en-US", {
|
||||||
notation: "compact",
|
notation: "compact",
|
||||||
}).format(usedTokens);
|
}).format(usedTokens);
|
||||||
const total = new Intl.NumberFormat("en-US", {
|
const total = new Intl.NumberFormat("en-US", {
|
||||||
notation: "compact",
|
notation: "compact",
|
||||||
}).format(maxTokens);
|
}).format(maxTokens);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
|
<div className={cn("w-full space-y-2 p-3", className)} {...props}>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-3 text-xs">
|
<div className="flex items-center justify-between gap-3 text-xs">
|
||||||
<p>{displayPct}</p>
|
<p>{displayPct}</p>
|
||||||
<p className="text-muted-foreground font-mono">
|
<p className="text-muted-foreground font-mono">
|
||||||
{used} / {total}
|
{used} / {total}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
|
<Progress className="bg-muted" value={usedPercent * PERCENT_MAX} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextContentBodyProps = ComponentProps<"div">;
|
export type ContextContentBodyProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextContentBody = ({
|
export const ContextContentBody = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ContextContentBodyProps) => (
|
}: ContextContentBodyProps) => (
|
||||||
<div className={cn("w-full p-3", className)} {...props}>
|
<div className={cn("w-full p-3", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ContextContentFooterProps = ComponentProps<"div">;
|
export type ContextContentFooterProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextContentFooter = ({
|
export const ContextContentFooter = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ContextContentFooterProps) => {
|
}: ContextContentFooterProps) => {
|
||||||
const { modelId, usage } = useContextValue();
|
const { modelId, usage } = useContextValue();
|
||||||
const costUSD = modelId
|
const costUSD = modelId
|
||||||
? getUsage({
|
? getUsage({
|
||||||
modelId,
|
modelId,
|
||||||
usage: {
|
usage: {
|
||||||
input: usage?.inputTokens ?? 0,
|
input: usage?.inputTokens ?? 0,
|
||||||
output: usage?.outputTokens ?? 0,
|
output: usage?.outputTokens ?? 0,
|
||||||
},
|
},
|
||||||
}).costUSD?.totalUSD
|
}).costUSD?.totalUSD
|
||||||
: undefined;
|
: undefined;
|
||||||
const totalCost = new Intl.NumberFormat("en-US", {
|
const totalCost = new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
}).format(costUSD ?? 0);
|
}).format(costUSD ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
|
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground">Total cost</span>
|
<span className="text-muted-foreground">Total cost</span>
|
||||||
<span>{totalCost}</span>
|
<span>{totalCost}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextInputUsageProps = ComponentProps<"div">;
|
export type ContextInputUsageProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextInputUsage = ({
|
export const ContextInputUsage = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ContextInputUsageProps) => {
|
}: ContextInputUsageProps) => {
|
||||||
const { usage, modelId } = useContextValue();
|
const { usage, modelId } = useContextValue();
|
||||||
const inputTokens = usage?.inputTokens ?? 0;
|
const inputTokens = usage?.inputTokens ?? 0;
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputTokens) {
|
if (!inputTokens) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputCost = modelId
|
const inputCost = modelId
|
||||||
? getUsage({
|
? getUsage({
|
||||||
modelId,
|
modelId,
|
||||||
usage: { input: inputTokens, output: 0 },
|
usage: { input: inputTokens, output: 0 },
|
||||||
}).costUSD?.totalUSD
|
}).costUSD?.totalUSD
|
||||||
: undefined;
|
: undefined;
|
||||||
const inputCostText = new Intl.NumberFormat("en-US", {
|
const inputCostText = new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
}).format(inputCost ?? 0);
|
}).format(inputCost ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center justify-between text-xs", className)}
|
className={cn("flex items-center justify-between text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">Input</span>
|
<span className="text-muted-foreground">Input</span>
|
||||||
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
|
<TokensWithCost costText={inputCostText} tokens={inputTokens} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextOutputUsageProps = ComponentProps<"div">;
|
export type ContextOutputUsageProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextOutputUsage = ({
|
export const ContextOutputUsage = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ContextOutputUsageProps) => {
|
}: ContextOutputUsageProps) => {
|
||||||
const { usage, modelId } = useContextValue();
|
const { usage, modelId } = useContextValue();
|
||||||
const outputTokens = usage?.outputTokens ?? 0;
|
const outputTokens = usage?.outputTokens ?? 0;
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!outputTokens) {
|
if (!outputTokens) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputCost = modelId
|
const outputCost = modelId
|
||||||
? getUsage({
|
? getUsage({
|
||||||
modelId,
|
modelId,
|
||||||
usage: { input: 0, output: outputTokens },
|
usage: { input: 0, output: outputTokens },
|
||||||
}).costUSD?.totalUSD
|
}).costUSD?.totalUSD
|
||||||
: undefined;
|
: undefined;
|
||||||
const outputCostText = new Intl.NumberFormat("en-US", {
|
const outputCostText = new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
}).format(outputCost ?? 0);
|
}).format(outputCost ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center justify-between text-xs", className)}
|
className={cn("flex items-center justify-between text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">Output</span>
|
<span className="text-muted-foreground">Output</span>
|
||||||
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
|
<TokensWithCost costText={outputCostText} tokens={outputTokens} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextReasoningUsageProps = ComponentProps<"div">;
|
export type ContextReasoningUsageProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextReasoningUsage = ({
|
export const ContextReasoningUsage = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ContextReasoningUsageProps) => {
|
}: ContextReasoningUsageProps) => {
|
||||||
const { usage, modelId } = useContextValue();
|
const { usage, modelId } = useContextValue();
|
||||||
const reasoningTokens = usage?.reasoningTokens ?? 0;
|
const reasoningTokens = usage?.reasoningTokens ?? 0;
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reasoningTokens) {
|
if (!reasoningTokens) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reasoningCost = modelId
|
const reasoningCost = modelId
|
||||||
? getUsage({
|
? getUsage({
|
||||||
modelId,
|
modelId,
|
||||||
usage: { reasoningTokens },
|
usage: { reasoningTokens },
|
||||||
}).costUSD?.totalUSD
|
}).costUSD?.totalUSD
|
||||||
: undefined;
|
: undefined;
|
||||||
const reasoningCostText = new Intl.NumberFormat("en-US", {
|
const reasoningCostText = new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
}).format(reasoningCost ?? 0);
|
}).format(reasoningCost ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center justify-between text-xs", className)}
|
className={cn("flex items-center justify-between text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">Reasoning</span>
|
<span className="text-muted-foreground">Reasoning</span>
|
||||||
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
|
<TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextCacheUsageProps = ComponentProps<"div">;
|
export type ContextCacheUsageProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ContextCacheUsage = ({
|
export const ContextCacheUsage = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ContextCacheUsageProps) => {
|
}: ContextCacheUsageProps) => {
|
||||||
const { usage, modelId } = useContextValue();
|
const { usage, modelId } = useContextValue();
|
||||||
const cacheTokens = usage?.cachedInputTokens ?? 0;
|
const cacheTokens = usage?.cachedInputTokens ?? 0;
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cacheTokens) {
|
if (!cacheTokens) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheCost = modelId
|
const cacheCost = modelId
|
||||||
? getUsage({
|
? getUsage({
|
||||||
modelId,
|
modelId,
|
||||||
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
|
usage: { cacheReads: cacheTokens, input: 0, output: 0 },
|
||||||
}).costUSD?.totalUSD
|
}).costUSD?.totalUSD
|
||||||
: undefined;
|
: undefined;
|
||||||
const cacheCostText = new Intl.NumberFormat("en-US", {
|
const cacheCostText = new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
}).format(cacheCost ?? 0);
|
}).format(cacheCost ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center justify-between text-xs", className)}
|
className={cn("flex items-center justify-between text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">Cache</span>
|
<span className="text-muted-foreground">Cache</span>
|
||||||
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
|
<TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TokensWithCost = ({
|
const TokensWithCost = ({
|
||||||
tokens,
|
tokens,
|
||||||
costText,
|
costText,
|
||||||
}: {
|
}: {
|
||||||
tokens?: number;
|
tokens?: number;
|
||||||
costText?: string;
|
costText?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<span>
|
<span>
|
||||||
{tokens === undefined
|
{tokens === undefined
|
||||||
? "—"
|
? "—"
|
||||||
: new Intl.NumberFormat("en-US", {
|
: new Intl.NumberFormat("en-US", {
|
||||||
notation: "compact",
|
notation: "compact",
|
||||||
}).format(tokens)}
|
}).format(tokens)}
|
||||||
{costText ? (
|
{costText ? (
|
||||||
<span className="text-muted-foreground ml-2">• {costText}</span>
|
<span className="text-muted-foreground ml-2">• {costText}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Controls as ControlsPrimitive } from "@xyflow/react";
|
import { Controls as ControlsPrimitive } from "@xyflow/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
||||||
|
|
||||||
export const Controls = ({ className, ...props }: ControlsProps) => (
|
export const Controls = ({ className, ...props }: ControlsProps) => (
|
||||||
<ControlsPrimitive
|
<ControlsPrimitive
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
|
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
|
||||||
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
|
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,100 +1,100 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
import { ArrowDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||||
|
|
||||||
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
<StickToBottom
|
<StickToBottom
|
||||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
className={cn("relative flex-1 overflow-y-hidden", className)}
|
||||||
initial="smooth"
|
initial="smooth"
|
||||||
resize="smooth"
|
resize="smooth"
|
||||||
role="log"
|
role="log"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ConversationContentProps = ComponentProps<
|
export type ConversationContentProps = ComponentProps<
|
||||||
typeof StickToBottom.Content
|
typeof StickToBottom.Content
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ConversationContent = ({
|
export const ConversationContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ConversationContentProps) => (
|
}: ConversationContentProps) => (
|
||||||
<StickToBottom.Content
|
<StickToBottom.Content
|
||||||
className={cn("flex flex-col gap-8 p-4", className)}
|
className={cn("flex flex-col gap-8 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
export type ConversationEmptyStateProps = ComponentProps<"div"> & {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConversationEmptyState = ({
|
export const ConversationEmptyState = ({
|
||||||
className,
|
className,
|
||||||
title = "No messages yet",
|
title = "No messages yet",
|
||||||
description = "Start a conversation to see messages here",
|
description = "Start a conversation to see messages here",
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ConversationEmptyStateProps) => (
|
}: ConversationEmptyStateProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
"flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className="text-sm font-medium">{title}</h3>
|
<h3 className="text-sm font-medium">{title}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-muted-foreground text-sm">{description}</p>
|
<p className="text-muted-foreground text-sm">{description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const ConversationScrollButton = ({
|
export const ConversationScrollButton = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ConversationScrollButtonProps) => {
|
}: ConversationScrollButtonProps) => {
|
||||||
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||||
|
|
||||||
const handleScrollToBottom = useCallback(() => {
|
const handleScrollToBottom = useCallback(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [scrollToBottom]);
|
}, [scrollToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isAtBottom && (
|
!isAtBottom && (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
|
"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleScrollToBottom}
|
onClick={handleScrollToBottom}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowDownIcon className="size-4" />
|
<ArrowDownIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,140 +1,140 @@
|
|||||||
import {
|
import {
|
||||||
BaseEdge,
|
BaseEdge,
|
||||||
type EdgeProps,
|
type EdgeProps,
|
||||||
getBezierPath,
|
getBezierPath,
|
||||||
getSimpleBezierPath,
|
getSimpleBezierPath,
|
||||||
type InternalNode,
|
type InternalNode,
|
||||||
type Node,
|
type Node,
|
||||||
Position,
|
Position,
|
||||||
useInternalNode,
|
useInternalNode,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
|
|
||||||
const Temporary = ({
|
const Temporary = ({
|
||||||
id,
|
id,
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
}: EdgeProps) => {
|
}: EdgeProps) => {
|
||||||
const [edgePath] = getSimpleBezierPath({
|
const [edgePath] = getSimpleBezierPath({
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
className="stroke-ring stroke-1"
|
className="stroke-ring stroke-1"
|
||||||
id={id}
|
id={id}
|
||||||
path={edgePath}
|
path={edgePath}
|
||||||
style={{
|
style={{
|
||||||
strokeDasharray: "5, 5",
|
strokeDasharray: "5, 5",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHandleCoordsByPosition = (
|
const getHandleCoordsByPosition = (
|
||||||
node: InternalNode<Node>,
|
node: InternalNode<Node>,
|
||||||
handlePosition: Position,
|
handlePosition: Position,
|
||||||
) => {
|
) => {
|
||||||
// Choose the handle type based on position - Left is for target, Right is for source
|
// Choose the handle type based on position - Left is for target, Right is for source
|
||||||
const handleType = handlePosition === Position.Left ? "target" : "source";
|
const handleType = handlePosition === Position.Left ? "target" : "source";
|
||||||
|
|
||||||
const handle = node.internals.handleBounds?.[handleType]?.find(
|
const handle = node.internals.handleBounds?.[handleType]?.find(
|
||||||
(h) => h.position === handlePosition,
|
(h) => h.position === handlePosition,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
return [0, 0] as const;
|
return [0, 0] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsetX = handle.width / 2;
|
let offsetX = handle.width / 2;
|
||||||
let offsetY = handle.height / 2;
|
let offsetY = handle.height / 2;
|
||||||
|
|
||||||
// this is a tiny detail to make the markerEnd of an edge visible.
|
// this is a tiny detail to make the markerEnd of an edge visible.
|
||||||
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
||||||
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
||||||
switch (handlePosition) {
|
switch (handlePosition) {
|
||||||
case Position.Left:
|
case Position.Left:
|
||||||
offsetX = 0;
|
offsetX = 0;
|
||||||
break;
|
break;
|
||||||
case Position.Right:
|
case Position.Right:
|
||||||
offsetX = handle.width;
|
offsetX = handle.width;
|
||||||
break;
|
break;
|
||||||
case Position.Top:
|
case Position.Top:
|
||||||
offsetY = 0;
|
offsetY = 0;
|
||||||
break;
|
break;
|
||||||
case Position.Bottom:
|
case Position.Bottom:
|
||||||
offsetY = handle.height;
|
offsetY = handle.height;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid handle position: ${handlePosition}`);
|
throw new Error(`Invalid handle position: ${handlePosition}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
|
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
|
||||||
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
|
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
|
||||||
|
|
||||||
return [x, y] as const;
|
return [x, y] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEdgeParams = (
|
const getEdgeParams = (
|
||||||
source: InternalNode<Node>,
|
source: InternalNode<Node>,
|
||||||
target: InternalNode<Node>,
|
target: InternalNode<Node>,
|
||||||
) => {
|
) => {
|
||||||
const sourcePos = Position.Right;
|
const sourcePos = Position.Right;
|
||||||
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
||||||
const targetPos = Position.Left;
|
const targetPos = Position.Left;
|
||||||
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
|
const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sx,
|
sx,
|
||||||
sy,
|
sy,
|
||||||
tx,
|
tx,
|
||||||
ty,
|
ty,
|
||||||
sourcePos,
|
sourcePos,
|
||||||
targetPos,
|
targetPos,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
||||||
const sourceNode = useInternalNode(source);
|
const sourceNode = useInternalNode(source);
|
||||||
const targetNode = useInternalNode(target);
|
const targetNode = useInternalNode(target);
|
||||||
|
|
||||||
if (!(sourceNode && targetNode)) {
|
if (!(sourceNode && targetNode)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
||||||
sourceNode,
|
sourceNode,
|
||||||
targetNode,
|
targetNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [edgePath] = getBezierPath({
|
const [edgePath] = getBezierPath({
|
||||||
sourceX: sx,
|
sourceX: sx,
|
||||||
sourceY: sy,
|
sourceY: sy,
|
||||||
sourcePosition: sourcePos,
|
sourcePosition: sourcePos,
|
||||||
targetX: tx,
|
targetX: tx,
|
||||||
targetY: ty,
|
targetY: ty,
|
||||||
targetPosition: targetPos,
|
targetPosition: targetPos,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
|
<BaseEdge id={id} markerEnd={markerEnd} path={edgePath} style={style} />
|
||||||
<circle fill="var(--primary)" r="4">
|
<circle fill="var(--primary)" r="4">
|
||||||
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
|
<animateMotion dur="2s" path={edgePath} repeatCount="indefinite" />
|
||||||
</circle>
|
</circle>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Edge = {
|
export const Edge = {
|
||||||
Temporary,
|
Temporary,
|
||||||
Animated,
|
Animated,
|
||||||
};
|
};
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Experimental_GeneratedImage } from "ai";
|
import type { Experimental_GeneratedImage } from "ai";
|
||||||
|
|
||||||
export type ImageProps = Experimental_GeneratedImage & {
|
export type ImageProps = Experimental_GeneratedImage & {
|
||||||
className?: string;
|
className?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Image = ({
|
export const Image = ({
|
||||||
base64,
|
base64,
|
||||||
uint8Array,
|
uint8Array,
|
||||||
mediaType,
|
mediaType,
|
||||||
...props
|
...props
|
||||||
}: ImageProps) => (
|
}: ImageProps) => (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
alt={props.alt}
|
alt={props.alt}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto max-w-full overflow-hidden rounded-md",
|
"h-auto max-w-full overflow-hidden rounded-md",
|
||||||
props.className,
|
props.className,
|
||||||
)}
|
)}
|
||||||
src={`data:${mediaType};base64,${base64}`}
|
src={`data:${mediaType};base64,${base64}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,96 +1,96 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { HTMLAttributes } from "react";
|
import type { HTMLAttributes } from "react";
|
||||||
|
|
||||||
type LoaderIconProps = {
|
type LoaderIconProps = {
|
||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
||||||
<svg
|
<svg
|
||||||
height={size}
|
height={size}
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
style={{ color: "currentcolor" }}
|
style={{ color: "currentcolor" }}
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
width={size}
|
width={size}
|
||||||
>
|
>
|
||||||
<title>Loader</title>
|
<title>Loader</title>
|
||||||
<g clipPath="url(#clip0_2393_1490)">
|
<g clipPath="url(#clip0_2393_1490)">
|
||||||
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
||||||
<path
|
<path
|
||||||
d="M8 16V12"
|
d="M8 16V12"
|
||||||
opacity="0.5"
|
opacity="0.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M3.29773 1.52783L5.64887 4.7639"
|
d="M3.29773 1.52783L5.64887 4.7639"
|
||||||
opacity="0.9"
|
opacity="0.9"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12.7023 1.52783L10.3511 4.7639"
|
d="M12.7023 1.52783L10.3511 4.7639"
|
||||||
opacity="0.1"
|
opacity="0.1"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12.7023 14.472L10.3511 11.236"
|
d="M12.7023 14.472L10.3511 11.236"
|
||||||
opacity="0.4"
|
opacity="0.4"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M3.29773 14.472L5.64887 11.236"
|
d="M3.29773 14.472L5.64887 11.236"
|
||||||
opacity="0.6"
|
opacity="0.6"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M15.6085 5.52783L11.8043 6.7639"
|
d="M15.6085 5.52783L11.8043 6.7639"
|
||||||
opacity="0.2"
|
opacity="0.2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M0.391602 10.472L4.19583 9.23598"
|
d="M0.391602 10.472L4.19583 9.23598"
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M15.6085 10.4722L11.8043 9.2361"
|
d="M15.6085 10.4722L11.8043 9.2361"
|
||||||
opacity="0.3"
|
opacity="0.3"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M0.391602 5.52783L4.19583 6.7639"
|
d="M0.391602 5.52783L4.19583 6.7639"
|
||||||
opacity="0.8"
|
opacity="0.8"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_2393_1490">
|
<clipPath id="clip0_2393_1490">
|
||||||
<rect fill="white" height="16" width="16" />
|
<rect fill="white" height="16" width="16" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex animate-spin items-center justify-center",
|
"inline-flex animate-spin items-center justify-center",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<LoaderIcon size={size} />
|
<LoaderIcon size={size} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,446 +1,446 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
|
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { FileUIPart, UIMessage } from "ai";
|
import type { FileUIPart, UIMessage } from "ai";
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
PaperclipIcon,
|
PaperclipIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
from: UIMessage["role"];
|
from: UIMessage["role"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex w-full flex-col gap-2",
|
"group flex w-full flex-col gap-2",
|
||||||
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const MessageContent = ({
|
export const MessageContent = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: MessageContentProps) => (
|
}: MessageContentProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-visible",
|
"is-user:dark flex w-fit max-w-full min-w-0 flex-col gap-2 overflow-visible",
|
||||||
"group-[.is-user]:overflow-hidden",
|
"group-[.is-user]:overflow-hidden",
|
||||||
"group-[.is-user]:bg-secondary group-[.is-user]:text-foreground group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:px-4 group-[.is-user]:py-3",
|
"group-[.is-user]:bg-secondary group-[.is-user]:text-foreground group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:px-4 group-[.is-user]:py-3",
|
||||||
"group-[.is-assistant]:text-foreground",
|
"group-[.is-assistant]:text-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type MessageActionsProps = ComponentProps<"div">;
|
export type MessageActionsProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const MessageActions = ({
|
export const MessageActions = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MessageActionsProps) => (
|
}: MessageActionsProps) => (
|
||||||
<div className={cn("flex items-center gap-1", className)} {...props}>
|
<div className={cn("flex items-center gap-1", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type MessageActionProps = ComponentProps<typeof Button> & {
|
export type MessageActionProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageAction = ({
|
export const MessageAction = ({
|
||||||
tooltip,
|
tooltip,
|
||||||
children,
|
children,
|
||||||
label,
|
label,
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "icon-sm",
|
size = "icon-sm",
|
||||||
...props
|
...props
|
||||||
}: MessageActionProps) => {
|
}: MessageActionProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<Button size={size} type="button" variant={variant} {...props}>
|
<Button size={size} type="button" variant={variant} {...props}>
|
||||||
{children}
|
{children}
|
||||||
<span className="sr-only">{label || tooltip}</span>
|
<span className="sr-only">{label || tooltip}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{tooltip}</p>
|
<p>{tooltip}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MessageBranchContextType = {
|
type MessageBranchContextType = {
|
||||||
currentBranch: number;
|
currentBranch: number;
|
||||||
totalBranches: number;
|
totalBranches: number;
|
||||||
goToPrevious: () => void;
|
goToPrevious: () => void;
|
||||||
goToNext: () => void;
|
goToNext: () => void;
|
||||||
branches: ReactElement[];
|
branches: ReactElement[];
|
||||||
setBranches: (branches: ReactElement[]) => void;
|
setBranches: (branches: ReactElement[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const useMessageBranch = () => {
|
const useMessageBranch = () => {
|
||||||
const context = useContext(MessageBranchContext);
|
const context = useContext(MessageBranchContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"MessageBranch components must be used within MessageBranch",
|
"MessageBranch components must be used within MessageBranch",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
defaultBranch?: number;
|
defaultBranch?: number;
|
||||||
onBranchChange?: (branchIndex: number) => void;
|
onBranchChange?: (branchIndex: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageBranch = ({
|
export const MessageBranch = ({
|
||||||
defaultBranch = 0,
|
defaultBranch = 0,
|
||||||
onBranchChange,
|
onBranchChange,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchProps) => {
|
}: MessageBranchProps) => {
|
||||||
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||||
const [branches, setBranches] = useState<ReactElement[]>([]);
|
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||||
|
|
||||||
const handleBranchChange = (newBranch: number) => {
|
const handleBranchChange = (newBranch: number) => {
|
||||||
setCurrentBranch(newBranch);
|
setCurrentBranch(newBranch);
|
||||||
onBranchChange?.(newBranch);
|
onBranchChange?.(newBranch);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPrevious = () => {
|
const goToPrevious = () => {
|
||||||
const newBranch =
|
const newBranch =
|
||||||
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||||
handleBranchChange(newBranch);
|
handleBranchChange(newBranch);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToNext = () => {
|
const goToNext = () => {
|
||||||
const newBranch =
|
const newBranch =
|
||||||
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||||
handleBranchChange(newBranch);
|
handleBranchChange(newBranch);
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextValue: MessageBranchContextType = {
|
const contextValue: MessageBranchContextType = {
|
||||||
currentBranch,
|
currentBranch,
|
||||||
totalBranches: branches.length,
|
totalBranches: branches.length,
|
||||||
goToPrevious,
|
goToPrevious,
|
||||||
goToNext,
|
goToNext,
|
||||||
branches,
|
branches,
|
||||||
setBranches,
|
setBranches,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageBranchContext.Provider value={contextValue}>
|
<MessageBranchContext.Provider value={contextValue}>
|
||||||
<div
|
<div
|
||||||
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</MessageBranchContext.Provider>
|
</MessageBranchContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const MessageBranchContent = ({
|
export const MessageBranchContent = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchContentProps) => {
|
}: MessageBranchContentProps) => {
|
||||||
const { currentBranch, setBranches, branches } = useMessageBranch();
|
const { currentBranch, setBranches, branches } = useMessageBranch();
|
||||||
const childrenArray = Array.isArray(children) ? children : [children];
|
const childrenArray = Array.isArray(children) ? children : [children];
|
||||||
|
|
||||||
// Use useEffect to update branches when they change
|
// Use useEffect to update branches when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (branches.length !== childrenArray.length) {
|
if (branches.length !== childrenArray.length) {
|
||||||
setBranches(childrenArray);
|
setBranches(childrenArray);
|
||||||
}
|
}
|
||||||
}, [childrenArray, branches, setBranches]);
|
}, [childrenArray, branches, setBranches]);
|
||||||
|
|
||||||
return childrenArray.map((branch, index) => (
|
return childrenArray.map((branch, index) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
||||||
index === currentBranch ? "block" : "hidden",
|
index === currentBranch ? "block" : "hidden",
|
||||||
)}
|
)}
|
||||||
key={branch.key}
|
key={branch.key}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{branch}
|
{branch}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
from: UIMessage["role"];
|
from: UIMessage["role"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageBranchSelector = ({
|
export const MessageBranchSelector = ({
|
||||||
className,
|
className,
|
||||||
from,
|
from,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchSelectorProps) => {
|
}: MessageBranchSelectorProps) => {
|
||||||
const { totalBranches } = useMessageBranch();
|
const { totalBranches } = useMessageBranch();
|
||||||
|
|
||||||
// Don't render if there's only one branch
|
// Don't render if there's only one branch
|
||||||
if (totalBranches <= 1) {
|
if (totalBranches <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
|
className="[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const MessageBranchPrevious = ({
|
export const MessageBranchPrevious = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchPreviousProps) => {
|
}: MessageBranchPreviousProps) => {
|
||||||
const { goToPrevious, totalBranches } = useMessageBranch();
|
const { goToPrevious, totalBranches } = useMessageBranch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Previous branch"
|
aria-label="Previous branch"
|
||||||
disabled={totalBranches <= 1}
|
disabled={totalBranches <= 1}
|
||||||
onClick={goToPrevious}
|
onClick={goToPrevious}
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronLeftIcon size={14} />}
|
{children ?? <ChevronLeftIcon size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
export const MessageBranchNext = ({
|
export const MessageBranchNext = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchNextProps) => {
|
}: MessageBranchNextProps) => {
|
||||||
const { goToNext, totalBranches } = useMessageBranch();
|
const { goToNext, totalBranches } = useMessageBranch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Next branch"
|
aria-label="Next branch"
|
||||||
disabled={totalBranches <= 1}
|
disabled={totalBranches <= 1}
|
||||||
onClick={goToNext}
|
onClick={goToNext}
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRightIcon size={14} />}
|
{children ?? <ChevronRightIcon size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
export const MessageBranchPage = ({
|
export const MessageBranchPage = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: MessageBranchPageProps) => {
|
}: MessageBranchPageProps) => {
|
||||||
const { currentBranch, totalBranches } = useMessageBranch();
|
const { currentBranch, totalBranches } = useMessageBranch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroupText
|
<ButtonGroupText
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground border-none bg-transparent shadow-none",
|
"text-muted-foreground border-none bg-transparent shadow-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{currentBranch + 1} of {totalBranches}
|
{currentBranch + 1} of {totalBranches}
|
||||||
</ButtonGroupText>
|
</ButtonGroupText>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||||
|
|
||||||
export const MessageResponse = memo(
|
export const MessageResponse = memo(
|
||||||
({ className, ...props }: MessageResponseProps) => (
|
({ className, ...props }: MessageResponseProps) => (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
(prevProps, nextProps) => prevProps.children === nextProps.children,
|
(prevProps, nextProps) => prevProps.children === nextProps.children,
|
||||||
);
|
);
|
||||||
|
|
||||||
MessageResponse.displayName = "MessageResponse";
|
MessageResponse.displayName = "MessageResponse";
|
||||||
|
|
||||||
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
|
export type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
data: FileUIPart;
|
data: FileUIPart;
|
||||||
className?: string;
|
className?: string;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MessageAttachment({
|
export function MessageAttachment({
|
||||||
data,
|
data,
|
||||||
className,
|
className,
|
||||||
onRemove,
|
onRemove,
|
||||||
...props
|
...props
|
||||||
}: MessageAttachmentProps) {
|
}: MessageAttachmentProps) {
|
||||||
const filename = data.filename || "";
|
const filename = data.filename || "";
|
||||||
const mediaType =
|
const mediaType =
|
||||||
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
||||||
const isImage = mediaType === "image";
|
const isImage = mediaType === "image";
|
||||||
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
|
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative size-24 overflow-hidden rounded-lg",
|
"group relative size-24 overflow-hidden rounded-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
alt={filename || "attachment"}
|
alt={filename || "attachment"}
|
||||||
className="size-full object-cover"
|
className="size-full object-cover"
|
||||||
height={100}
|
height={100}
|
||||||
src={data.url}
|
src={data.url}
|
||||||
width={100}
|
width={100}
|
||||||
/>
|
/>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Remove attachment"
|
aria-label="Remove attachment"
|
||||||
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove();
|
onRemove();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg">
|
<div className="bg-muted text-muted-foreground flex size-full shrink-0 items-center justify-center rounded-lg">
|
||||||
<PaperclipIcon className="size-4" />
|
<PaperclipIcon className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{attachmentLabel}</p>
|
<p>{attachmentLabel}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Remove attachment"
|
aria-label="Remove attachment"
|
||||||
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove();
|
onRemove();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageAttachmentsProps = ComponentProps<"div">;
|
export type MessageAttachmentsProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export function MessageAttachments({
|
export function MessageAttachments({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: MessageAttachmentsProps) {
|
}: MessageAttachmentsProps) {
|
||||||
if (!children) {
|
if (!children) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto flex w-fit flex-wrap items-start gap-2",
|
"ml-auto flex w-fit flex-wrap items-start gap-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageToolbarProps = ComponentProps<"div">;
|
export type MessageToolbarProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const MessageToolbar = ({
|
export const MessageToolbar = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MessageToolbarProps) => (
|
}: MessageToolbarProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-4 flex w-full items-center justify-between gap-4",
|
"mt-4 flex w-full items-center justify-between gap-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,208 +1,208 @@
|
|||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
export type ModelSelectorProps = ComponentProps<typeof Dialog>;
|
export type ModelSelectorProps = ComponentProps<typeof Dialog>;
|
||||||
|
|
||||||
export const ModelSelector = (props: ModelSelectorProps) => (
|
export const ModelSelector = (props: ModelSelectorProps) => (
|
||||||
<Dialog {...props} />
|
<Dialog {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
|
export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
|
||||||
|
|
||||||
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
|
export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
|
||||||
<DialogTrigger {...props} />
|
<DialogTrigger {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
|
export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelSelectorContent = ({
|
export const ModelSelectorContent = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
title = "Model Selector",
|
title = "Model Selector",
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorContentProps) => (
|
}: ModelSelectorContentProps) => (
|
||||||
<DialogContent className={cn("p-0", className)} {...props}>
|
<DialogContent className={cn("p-0", className)} {...props}>
|
||||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||||
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
|
export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
|
||||||
|
|
||||||
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
|
export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
|
||||||
<CommandDialog {...props} />
|
<CommandDialog {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
|
export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
|
||||||
|
|
||||||
export const ModelSelectorInput = ({
|
export const ModelSelectorInput = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorInputProps) => (
|
}: ModelSelectorInputProps) => (
|
||||||
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
|
<CommandInput className={cn("h-auto py-3.5", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
|
export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
|
||||||
|
|
||||||
export const ModelSelectorList = (props: ModelSelectorListProps) => (
|
export const ModelSelectorList = (props: ModelSelectorListProps) => (
|
||||||
<CommandList {...props} />
|
<CommandList {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
|
export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
|
||||||
|
|
||||||
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
|
export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
|
||||||
<CommandEmpty {...props} />
|
<CommandEmpty {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
|
export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
|
||||||
|
|
||||||
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
|
export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
|
||||||
<CommandGroup {...props} />
|
<CommandGroup {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
|
export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
|
||||||
|
|
||||||
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
|
export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
|
||||||
<CommandItem {...props} />
|
<CommandItem {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
|
export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
|
||||||
|
|
||||||
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
|
export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
|
||||||
<CommandShortcut {...props} />
|
<CommandShortcut {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorSeparatorProps = ComponentProps<
|
export type ModelSelectorSeparatorProps = ComponentProps<
|
||||||
typeof CommandSeparator
|
typeof CommandSeparator
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
|
||||||
<CommandSeparator {...props} />
|
<CommandSeparator {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorLogoProps = Omit<
|
export type ModelSelectorLogoProps = Omit<
|
||||||
ComponentProps<"img">,
|
ComponentProps<"img">,
|
||||||
"src" | "alt"
|
"src" | "alt"
|
||||||
> & {
|
> & {
|
||||||
provider:
|
provider:
|
||||||
| "moonshotai-cn"
|
| "moonshotai-cn"
|
||||||
| "lucidquery"
|
| "lucidquery"
|
||||||
| "moonshotai"
|
| "moonshotai"
|
||||||
| "zai-coding-plan"
|
| "zai-coding-plan"
|
||||||
| "alibaba"
|
| "alibaba"
|
||||||
| "xai"
|
| "xai"
|
||||||
| "vultr"
|
| "vultr"
|
||||||
| "nvidia"
|
| "nvidia"
|
||||||
| "upstage"
|
| "upstage"
|
||||||
| "groq"
|
| "groq"
|
||||||
| "github-copilot"
|
| "github-copilot"
|
||||||
| "mistral"
|
| "mistral"
|
||||||
| "vercel"
|
| "vercel"
|
||||||
| "nebius"
|
| "nebius"
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "alibaba-cn"
|
| "alibaba-cn"
|
||||||
| "google-vertex-anthropic"
|
| "google-vertex-anthropic"
|
||||||
| "venice"
|
| "venice"
|
||||||
| "chutes"
|
| "chutes"
|
||||||
| "cortecs"
|
| "cortecs"
|
||||||
| "github-models"
|
| "github-models"
|
||||||
| "togetherai"
|
| "togetherai"
|
||||||
| "azure"
|
| "azure"
|
||||||
| "baseten"
|
| "baseten"
|
||||||
| "huggingface"
|
| "huggingface"
|
||||||
| "opencode"
|
| "opencode"
|
||||||
| "fastrouter"
|
| "fastrouter"
|
||||||
| "google"
|
| "google"
|
||||||
| "google-vertex"
|
| "google-vertex"
|
||||||
| "cloudflare-workers-ai"
|
| "cloudflare-workers-ai"
|
||||||
| "inception"
|
| "inception"
|
||||||
| "wandb"
|
| "wandb"
|
||||||
| "openai"
|
| "openai"
|
||||||
| "zhipuai-coding-plan"
|
| "zhipuai-coding-plan"
|
||||||
| "perplexity"
|
| "perplexity"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "zenmux"
|
| "zenmux"
|
||||||
| "v0"
|
| "v0"
|
||||||
| "iflowcn"
|
| "iflowcn"
|
||||||
| "synthetic"
|
| "synthetic"
|
||||||
| "deepinfra"
|
| "deepinfra"
|
||||||
| "zhipuai"
|
| "zhipuai"
|
||||||
| "submodel"
|
| "submodel"
|
||||||
| "zai"
|
| "zai"
|
||||||
| "inference"
|
| "inference"
|
||||||
| "requesty"
|
| "requesty"
|
||||||
| "morph"
|
| "morph"
|
||||||
| "lmstudio"
|
| "lmstudio"
|
||||||
| "anthropic"
|
| "anthropic"
|
||||||
| "aihubmix"
|
| "aihubmix"
|
||||||
| "fireworks-ai"
|
| "fireworks-ai"
|
||||||
| "modelscope"
|
| "modelscope"
|
||||||
| "llama"
|
| "llama"
|
||||||
| "scaleway"
|
| "scaleway"
|
||||||
| "amazon-bedrock"
|
| "amazon-bedrock"
|
||||||
| "cerebras"
|
| "cerebras"
|
||||||
| (string & {});
|
| (string & {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelSelectorLogo = ({
|
export const ModelSelectorLogo = ({
|
||||||
provider,
|
provider,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorLogoProps) => (
|
}: ModelSelectorLogoProps) => (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
alt={`${provider} logo`}
|
alt={`${provider} logo`}
|
||||||
className={cn("size-3 dark:invert", className)}
|
className={cn("size-3 dark:invert", className)}
|
||||||
height={12}
|
height={12}
|
||||||
src={`https://models.dev/logos/${provider}.svg`}
|
src={`https://models.dev/logos/${provider}.svg`}
|
||||||
width={12}
|
width={12}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
|
export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const ModelSelectorLogoGroup = ({
|
export const ModelSelectorLogoGroup = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorLogoGroupProps) => (
|
}: ModelSelectorLogoGroupProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"[&>img]:bg-background dark:[&>img]:bg-foreground flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1",
|
"[&>img]:bg-background dark:[&>img]:bg-foreground flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModelSelectorNameProps = ComponentProps<"span">;
|
export type ModelSelectorNameProps = ComponentProps<"span">;
|
||||||
|
|
||||||
export const ModelSelectorName = ({
|
export const ModelSelectorName = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ModelSelectorNameProps) => (
|
}: ModelSelectorNameProps) => (
|
||||||
<span
|
<span
|
||||||
className={cn("flex-1 truncate text-left text-xs", className)}
|
className={cn("flex-1 truncate text-left text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,71 +1,71 @@
|
|||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Handle, Position } from "@xyflow/react";
|
import { Handle, Position } from "@xyflow/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type NodeProps = ComponentProps<typeof Card> & {
|
export type NodeProps = ComponentProps<typeof Card> & {
|
||||||
handles: {
|
handles: {
|
||||||
target: boolean;
|
target: boolean;
|
||||||
source: boolean;
|
source: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Node = ({ handles, className, ...props }: NodeProps) => (
|
export const Node = ({ handles, className, ...props }: NodeProps) => (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{handles.target && <Handle position={Position.Left} type="target" />}
|
{handles.target && <Handle position={Position.Left} type="target" />}
|
||||||
{handles.source && <Handle position={Position.Right} type="source" />}
|
{handles.source && <Handle position={Position.Right} type="source" />}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
||||||
|
|
||||||
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
|
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type NodeTitleProps = ComponentProps<typeof CardTitle>;
|
export type NodeTitleProps = ComponentProps<typeof CardTitle>;
|
||||||
|
|
||||||
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
|
export const NodeTitle = (props: NodeTitleProps) => <CardTitle {...props} />;
|
||||||
|
|
||||||
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
|
export type NodeDescriptionProps = ComponentProps<typeof CardDescription>;
|
||||||
|
|
||||||
export const NodeDescription = (props: NodeDescriptionProps) => (
|
export const NodeDescription = (props: NodeDescriptionProps) => (
|
||||||
<CardDescription {...props} />
|
<CardDescription {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type NodeActionProps = ComponentProps<typeof CardAction>;
|
export type NodeActionProps = ComponentProps<typeof CardAction>;
|
||||||
|
|
||||||
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
|
export const NodeAction = (props: NodeActionProps) => <CardAction {...props} />;
|
||||||
|
|
||||||
export type NodeContentProps = ComponentProps<typeof CardContent>;
|
export type NodeContentProps = ComponentProps<typeof CardContent>;
|
||||||
|
|
||||||
export const NodeContent = ({ className, ...props }: NodeContentProps) => (
|
export const NodeContent = ({ className, ...props }: NodeContentProps) => (
|
||||||
<CardContent className={cn("p-3", className)} {...props} />
|
<CardContent className={cn("p-3", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
||||||
|
|
||||||
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
||||||
<CardFooter
|
<CardFooter
|
||||||
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
|
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,365 +1,365 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
MessageCircleIcon,
|
MessageCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { type ComponentProps, createContext, useContext } from "react";
|
import { type ComponentProps, createContext, useContext } from "react";
|
||||||
|
|
||||||
const providers = {
|
const providers = {
|
||||||
github: {
|
github: {
|
||||||
title: "Open in GitHub",
|
title: "Open in GitHub",
|
||||||
createUrl: (url: string) => url,
|
createUrl: (url: string) => url,
|
||||||
icon: (
|
icon: (
|
||||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
|
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
|
||||||
<title>GitHub</title>
|
<title>GitHub</title>
|
||||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
scira: {
|
scira: {
|
||||||
title: "Open in Scira",
|
title: "Open in Scira",
|
||||||
createUrl: (q: string) =>
|
createUrl: (q: string) =>
|
||||||
`https://scira.ai/?${new URLSearchParams({
|
`https://scira.ai/?${new URLSearchParams({
|
||||||
q,
|
q,
|
||||||
})}`,
|
})}`,
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
height="934"
|
height="934"
|
||||||
viewBox="0 0 910 934"
|
viewBox="0 0 910 934"
|
||||||
width="910"
|
width="910"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<title>Scira AI</title>
|
<title>Scira AI</title>
|
||||||
<path
|
<path
|
||||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="20"
|
strokeWidth="20"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="20"
|
strokeWidth="20"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="30"
|
strokeWidth="30"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
chatgpt: {
|
chatgpt: {
|
||||||
title: "Open in ChatGPT",
|
title: "Open in ChatGPT",
|
||||||
createUrl: (prompt: string) =>
|
createUrl: (prompt: string) =>
|
||||||
`https://chatgpt.com/?${new URLSearchParams({
|
`https://chatgpt.com/?${new URLSearchParams({
|
||||||
hints: "search",
|
hints: "search",
|
||||||
prompt,
|
prompt,
|
||||||
})}`,
|
})}`,
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<title>OpenAI</title>
|
<title>OpenAI</title>
|
||||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
claude: {
|
claude: {
|
||||||
title: "Open in Claude",
|
title: "Open in Claude",
|
||||||
createUrl: (q: string) =>
|
createUrl: (q: string) =>
|
||||||
`https://claude.ai/new?${new URLSearchParams({
|
`https://claude.ai/new?${new URLSearchParams({
|
||||||
q,
|
q,
|
||||||
})}`,
|
})}`,
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
role="img"
|
role="img"
|
||||||
viewBox="0 0 12 12"
|
viewBox="0 0 12 12"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<title>Claude</title>
|
<title>Claude</title>
|
||||||
<path
|
<path
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
|
d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
t3: {
|
t3: {
|
||||||
title: "Open in T3 Chat",
|
title: "Open in T3 Chat",
|
||||||
createUrl: (q: string) =>
|
createUrl: (q: string) =>
|
||||||
`https://t3.chat/new?${new URLSearchParams({
|
`https://t3.chat/new?${new URLSearchParams({
|
||||||
q,
|
q,
|
||||||
})}`,
|
})}`,
|
||||||
icon: <MessageCircleIcon />,
|
icon: <MessageCircleIcon />,
|
||||||
},
|
},
|
||||||
v0: {
|
v0: {
|
||||||
title: "Open in v0",
|
title: "Open in v0",
|
||||||
createUrl: (q: string) =>
|
createUrl: (q: string) =>
|
||||||
`https://v0.app?${new URLSearchParams({
|
`https://v0.app?${new URLSearchParams({
|
||||||
q,
|
q,
|
||||||
})}`,
|
})}`,
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 147 70"
|
viewBox="0 0 147 70"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<title>v0</title>
|
<title>v0</title>
|
||||||
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
|
<path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z" />
|
||||||
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
|
<path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
cursor: {
|
cursor: {
|
||||||
title: "Open in Cursor",
|
title: "Open in Cursor",
|
||||||
createUrl: (text: string) => {
|
createUrl: (text: string) => {
|
||||||
const url = new URL("https://cursor.com/link/prompt");
|
const url = new URL("https://cursor.com/link/prompt");
|
||||||
url.searchParams.set("text", text);
|
url.searchParams.set("text", text);
|
||||||
return url.toString();
|
return url.toString();
|
||||||
},
|
},
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
viewBox="0 0 466.73 532.09"
|
viewBox="0 0 466.73 532.09"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<title>Cursor</title>
|
<title>Cursor</title>
|
||||||
<path
|
<path
|
||||||
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
|
d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
|
const OpenInContext = createContext<{ query: string } | undefined>(undefined);
|
||||||
|
|
||||||
const useOpenInContext = () => {
|
const useOpenInContext = () => {
|
||||||
const context = useContext(OpenInContext);
|
const context = useContext(OpenInContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("OpenIn components must be used within an OpenIn provider");
|
throw new Error("OpenIn components must be used within an OpenIn provider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
|
export type OpenInProps = ComponentProps<typeof DropdownMenu> & {
|
||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenIn = ({ query, ...props }: OpenInProps) => (
|
export const OpenIn = ({ query, ...props }: OpenInProps) => (
|
||||||
<OpenInContext.Provider value={{ query }}>
|
<OpenInContext.Provider value={{ query }}>
|
||||||
<DropdownMenu {...props} />
|
<DropdownMenu {...props} />
|
||||||
</OpenInContext.Provider>
|
</OpenInContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
|
export type OpenInContentProps = ComponentProps<typeof DropdownMenuContent>;
|
||||||
|
|
||||||
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
|
export const OpenInContent = ({ className, ...props }: OpenInContentProps) => (
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
className={cn("w-[240px]", className)}
|
className={cn("w-[240px]", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInItemProps = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInItem = (props: OpenInItemProps) => (
|
export const OpenInItem = (props: OpenInItemProps) => (
|
||||||
<DropdownMenuItem {...props} />
|
<DropdownMenuItem {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
|
export type OpenInLabelProps = ComponentProps<typeof DropdownMenuLabel>;
|
||||||
|
|
||||||
export const OpenInLabel = (props: OpenInLabelProps) => (
|
export const OpenInLabel = (props: OpenInLabelProps) => (
|
||||||
<DropdownMenuLabel {...props} />
|
<DropdownMenuLabel {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
|
export type OpenInSeparatorProps = ComponentProps<typeof DropdownMenuSeparator>;
|
||||||
|
|
||||||
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
|
export const OpenInSeparator = (props: OpenInSeparatorProps) => (
|
||||||
<DropdownMenuSeparator {...props} />
|
<DropdownMenuSeparator {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
|
export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
|
||||||
|
|
||||||
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
|
export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
|
||||||
<DropdownMenuTrigger {...props} asChild>
|
<DropdownMenuTrigger {...props} asChild>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<Button type="button" variant="outline">
|
<Button type="button" variant="outline">
|
||||||
Open in chat
|
Open in chat
|
||||||
<ChevronDownIcon className="size-4" />
|
<ChevronDownIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
|
export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.chatgpt.createUrl(query)}
|
href={providers.chatgpt.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.chatgpt.icon}</span>
|
<span className="shrink-0">{providers.chatgpt.icon}</span>
|
||||||
<span className="flex-1">{providers.chatgpt.title}</span>
|
<span className="flex-1">{providers.chatgpt.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInClaude = (props: OpenInClaudeProps) => {
|
export const OpenInClaude = (props: OpenInClaudeProps) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.claude.createUrl(query)}
|
href={providers.claude.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.claude.icon}</span>
|
<span className="shrink-0">{providers.claude.icon}</span>
|
||||||
<span className="flex-1">{providers.claude.title}</span>
|
<span className="flex-1">{providers.claude.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInT3 = (props: OpenInT3Props) => {
|
export const OpenInT3 = (props: OpenInT3Props) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.t3.createUrl(query)}
|
href={providers.t3.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.t3.icon}</span>
|
<span className="shrink-0">{providers.t3.icon}</span>
|
||||||
<span className="flex-1">{providers.t3.title}</span>
|
<span className="flex-1">{providers.t3.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInScira = (props: OpenInSciraProps) => {
|
export const OpenInScira = (props: OpenInSciraProps) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.scira.createUrl(query)}
|
href={providers.scira.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.scira.icon}</span>
|
<span className="shrink-0">{providers.scira.icon}</span>
|
||||||
<span className="flex-1">{providers.scira.title}</span>
|
<span className="flex-1">{providers.scira.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInv0 = (props: OpenInv0Props) => {
|
export const OpenInv0 = (props: OpenInv0Props) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.v0.createUrl(query)}
|
href={providers.v0.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.v0.icon}</span>
|
<span className="shrink-0">{providers.v0.icon}</span>
|
||||||
<span className="flex-1">{providers.v0.title}</span>
|
<span className="flex-1">{providers.v0.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
|
export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
|
||||||
|
|
||||||
export const OpenInCursor = (props: OpenInCursorProps) => {
|
export const OpenInCursor = (props: OpenInCursorProps) => {
|
||||||
const { query } = useOpenInContext();
|
const { query } = useOpenInContext();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem asChild {...props}>
|
<DropdownMenuItem asChild {...props}>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={providers.cursor.createUrl(query)}
|
href={providers.cursor.createUrl(query)}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span className="shrink-0">{providers.cursor.icon}</span>
|
<span className="shrink-0">{providers.cursor.icon}</span>
|
||||||
<span className="flex-1">{providers.cursor.title}</span>
|
<span className="flex-1">{providers.cursor.title}</span>
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Panel as PanelPrimitive } from "@xyflow/react";
|
import { Panel as PanelPrimitive } from "@xyflow/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
||||||
|
|
||||||
export const Panel = ({ className, ...props }: PanelProps) => (
|
export const Panel = ({ className, ...props }: PanelProps) => (
|
||||||
<PanelPrimitive
|
<PanelPrimitive
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card m-4 overflow-hidden rounded-md border p-1",
|
"bg-card m-4 overflow-hidden rounded-md border p-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,142 +1,142 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronsUpDownIcon } from "lucide-react";
|
import { ChevronsUpDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import { Shimmer } from "./shimmer";
|
import { Shimmer } from "./shimmer";
|
||||||
|
|
||||||
type PlanContextValue = {
|
type PlanContextValue = {
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlanContext = createContext<PlanContextValue | null>(null);
|
const PlanContext = createContext<PlanContextValue | null>(null);
|
||||||
|
|
||||||
const usePlan = () => {
|
const usePlan = () => {
|
||||||
const context = useContext(PlanContext);
|
const context = useContext(PlanContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Plan components must be used within Plan");
|
throw new Error("Plan components must be used within Plan");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanProps = ComponentProps<typeof Collapsible> & {
|
export type PlanProps = ComponentProps<typeof Collapsible> & {
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Plan = ({
|
export const Plan = ({
|
||||||
className,
|
className,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PlanProps) => (
|
}: PlanProps) => (
|
||||||
<PlanContext.Provider value={{ isStreaming }}>
|
<PlanContext.Provider value={{ isStreaming }}>
|
||||||
<Collapsible asChild data-slot="plan" {...props}>
|
<Collapsible asChild data-slot="plan" {...props}>
|
||||||
<Card className={cn("shadow-none", className)}>{children}</Card>
|
<Card className={cn("shadow-none", className)}>{children}</Card>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</PlanContext.Provider>
|
</PlanContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
|
export type PlanHeaderProps = ComponentProps<typeof CardHeader>;
|
||||||
|
|
||||||
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
|
export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => (
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className={cn("flex items-start justify-between", className)}
|
className={cn("flex items-start justify-between", className)}
|
||||||
data-slot="plan-header"
|
data-slot="plan-header"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PlanTitleProps = Omit<
|
export type PlanTitleProps = Omit<
|
||||||
ComponentProps<typeof CardTitle>,
|
ComponentProps<typeof CardTitle>,
|
||||||
"children"
|
"children"
|
||||||
> & {
|
> & {
|
||||||
children: string;
|
children: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
|
export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
|
||||||
const { isStreaming } = usePlan();
|
const { isStreaming } = usePlan();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardTitle data-slot="plan-title" {...props}>
|
<CardTitle data-slot="plan-title" {...props}>
|
||||||
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanDescriptionProps = Omit<
|
export type PlanDescriptionProps = Omit<
|
||||||
ComponentProps<typeof CardDescription>,
|
ComponentProps<typeof CardDescription>,
|
||||||
"children"
|
"children"
|
||||||
> & {
|
> & {
|
||||||
children: string;
|
children: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlanDescription = ({
|
export const PlanDescription = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PlanDescriptionProps) => {
|
}: PlanDescriptionProps) => {
|
||||||
const { isStreaming } = usePlan();
|
const { isStreaming } = usePlan();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardDescription
|
<CardDescription
|
||||||
className={cn("text-balance", className)}
|
className={cn("text-balance", className)}
|
||||||
data-slot="plan-description"
|
data-slot="plan-description"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
{isStreaming ? <Shimmer>{children}</Shimmer> : children}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanActionProps = ComponentProps<typeof CardAction>;
|
export type PlanActionProps = ComponentProps<typeof CardAction>;
|
||||||
|
|
||||||
export const PlanAction = (props: PlanActionProps) => (
|
export const PlanAction = (props: PlanActionProps) => (
|
||||||
<CardAction data-slot="plan-action" {...props} />
|
<CardAction data-slot="plan-action" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PlanContentProps = ComponentProps<typeof CardContent>;
|
export type PlanContentProps = ComponentProps<typeof CardContent>;
|
||||||
|
|
||||||
export const PlanContent = (props: PlanContentProps) => (
|
export const PlanContent = (props: PlanContentProps) => (
|
||||||
<CollapsibleContent asChild>
|
<CollapsibleContent asChild>
|
||||||
<CardContent data-slot="plan-content" {...props} />
|
<CardContent data-slot="plan-content" {...props} />
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PlanFooterProps = ComponentProps<"div">;
|
export type PlanFooterProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const PlanFooter = (props: PlanFooterProps) => (
|
export const PlanFooter = (props: PlanFooterProps) => (
|
||||||
<CardFooter data-slot="plan-footer" {...props} />
|
<CardFooter data-slot="plan-footer" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
export type PlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
|
||||||
|
|
||||||
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
|
export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => (
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={cn("size-8", className)}
|
className={cn("size-8", className)}
|
||||||
data-slot="plan-trigger"
|
data-slot="plan-trigger"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronsUpDownIcon className="size-4" />
|
<ChevronsUpDownIcon className="size-4" />
|
||||||
<span className="sr-only">Toggle plan</span>
|
<span className="sr-only">Toggle plan</span>
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
@@ -1,274 +1,274 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
|
import { ChevronDownIcon, PaperclipIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type QueueMessagePart = {
|
export type QueueMessagePart = {
|
||||||
type: string;
|
type: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
mediaType?: string;
|
mediaType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueueMessage = {
|
export type QueueMessage = {
|
||||||
id: string;
|
id: string;
|
||||||
parts: QueueMessagePart[];
|
parts: QueueMessagePart[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueueTodo = {
|
export type QueueTodo = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
status?: "pending" | "completed";
|
status?: "pending" | "completed";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueueItemProps = ComponentProps<"li">;
|
export type QueueItemProps = ComponentProps<"li">;
|
||||||
|
|
||||||
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
||||||
<li
|
<li
|
||||||
className={cn(
|
className={cn(
|
||||||
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
|
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemIndicatorProps = ComponentProps<"span"> & {
|
export type QueueItemIndicatorProps = ComponentProps<"span"> & {
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueueItemIndicator = ({
|
export const QueueItemIndicator = ({
|
||||||
completed = false,
|
completed = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemIndicatorProps) => (
|
}: QueueItemIndicatorProps) => (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-0.5 inline-block size-2.5 rounded-full border",
|
"mt-0.5 inline-block size-2.5 rounded-full border",
|
||||||
completed
|
completed
|
||||||
? "border-muted-foreground/20 bg-muted-foreground/10"
|
? "border-muted-foreground/20 bg-muted-foreground/10"
|
||||||
: "border-muted-foreground/50",
|
: "border-muted-foreground/50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemContentProps = ComponentProps<"span"> & {
|
export type QueueItemContentProps = ComponentProps<"span"> & {
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueueItemContent = ({
|
export const QueueItemContent = ({
|
||||||
completed = false,
|
completed = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemContentProps) => (
|
}: QueueItemContentProps) => (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"line-clamp-1 grow break-words",
|
"line-clamp-1 grow break-words",
|
||||||
completed
|
completed
|
||||||
? "text-muted-foreground/50 line-through"
|
? "text-muted-foreground/50 line-through"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemDescriptionProps = ComponentProps<"div"> & {
|
export type QueueItemDescriptionProps = ComponentProps<"div"> & {
|
||||||
completed?: boolean;
|
completed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueueItemDescription = ({
|
export const QueueItemDescription = ({
|
||||||
completed = false,
|
completed = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemDescriptionProps) => (
|
}: QueueItemDescriptionProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-6 text-xs",
|
"ml-6 text-xs",
|
||||||
completed
|
completed
|
||||||
? "text-muted-foreground/40 line-through"
|
? "text-muted-foreground/40 line-through"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemActionsProps = ComponentProps<"div">;
|
export type QueueItemActionsProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const QueueItemActions = ({
|
export const QueueItemActions = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemActionsProps) => (
|
}: QueueItemActionsProps) => (
|
||||||
<div className={cn("flex gap-1", className)} {...props} />
|
<div className={cn("flex gap-1", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemActionProps = Omit<
|
export type QueueItemActionProps = Omit<
|
||||||
ComponentProps<typeof Button>,
|
ComponentProps<typeof Button>,
|
||||||
"variant" | "size"
|
"variant" | "size"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const QueueItemAction = ({
|
export const QueueItemAction = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemActionProps) => (
|
}: QueueItemActionProps) => (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
|
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemAttachmentProps = ComponentProps<"div">;
|
export type QueueItemAttachmentProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const QueueItemAttachment = ({
|
export const QueueItemAttachment = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemAttachmentProps) => (
|
}: QueueItemAttachmentProps) => (
|
||||||
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
|
<div className={cn("mt-1 flex flex-wrap gap-2", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemImageProps = ComponentProps<"img">;
|
export type QueueItemImageProps = ComponentProps<"img">;
|
||||||
|
|
||||||
export const QueueItemImage = ({
|
export const QueueItemImage = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemImageProps) => (
|
}: QueueItemImageProps) => (
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className={cn("h-8 w-8 rounded border object-cover", className)}
|
className={cn("h-8 w-8 rounded border object-cover", className)}
|
||||||
height={32}
|
height={32}
|
||||||
width={32}
|
width={32}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueItemFileProps = ComponentProps<"span">;
|
export type QueueItemFileProps = ComponentProps<"span">;
|
||||||
|
|
||||||
export const QueueItemFile = ({
|
export const QueueItemFile = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueItemFileProps) => (
|
}: QueueItemFileProps) => (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
|
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PaperclipIcon size={12} />
|
<PaperclipIcon size={12} />
|
||||||
<span className="max-w-[100px] truncate">{children}</span>
|
<span className="max-w-[100px] truncate">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueListProps = ComponentProps<typeof ScrollArea>;
|
export type QueueListProps = ComponentProps<typeof ScrollArea>;
|
||||||
|
|
||||||
export const QueueList = ({
|
export const QueueList = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueListProps) => (
|
}: QueueListProps) => (
|
||||||
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
|
<ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
|
||||||
<div className="max-h-40 pr-4">
|
<div className="max-h-40 pr-4">
|
||||||
<ul>{children}</ul>
|
<ul>{children}</ul>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|
||||||
// QueueSection - collapsible section container
|
// QueueSection - collapsible section container
|
||||||
export type QueueSectionProps = ComponentProps<typeof Collapsible>;
|
export type QueueSectionProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
export const QueueSection = ({
|
export const QueueSection = ({
|
||||||
className,
|
className,
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
...props
|
...props
|
||||||
}: QueueSectionProps) => (
|
}: QueueSectionProps) => (
|
||||||
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
// QueueSectionTrigger - section header/trigger
|
// QueueSectionTrigger - section header/trigger
|
||||||
export type QueueSectionTriggerProps = ComponentProps<"button">;
|
export type QueueSectionTriggerProps = ComponentProps<"button">;
|
||||||
|
|
||||||
export const QueueSectionTrigger = ({
|
export const QueueSectionTrigger = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueSectionTriggerProps) => (
|
}: QueueSectionTriggerProps) => (
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
|
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
// QueueSectionLabel - label content with icon and count
|
// QueueSectionLabel - label content with icon and count
|
||||||
export type QueueSectionLabelProps = ComponentProps<"span"> & {
|
export type QueueSectionLabelProps = ComponentProps<"span"> & {
|
||||||
count?: number;
|
count?: number;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueueSectionLabel = ({
|
export const QueueSectionLabel = ({
|
||||||
count,
|
count,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueSectionLabelProps) => (
|
}: QueueSectionLabelProps) => (
|
||||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
<span className={cn("flex items-center gap-2", className)} {...props}>
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
||||||
{icon}
|
{icon}
|
||||||
<span>
|
<span>
|
||||||
{count} {label}
|
{count} {label}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
// QueueSectionContent - collapsible content area
|
// QueueSectionContent - collapsible content area
|
||||||
export type QueueSectionContentProps = ComponentProps<
|
export type QueueSectionContentProps = ComponentProps<
|
||||||
typeof CollapsibleContent
|
typeof CollapsibleContent
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const QueueSectionContent = ({
|
export const QueueSectionContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueSectionContentProps) => (
|
}: QueueSectionContentProps) => (
|
||||||
<CollapsibleContent className={cn(className)} {...props} />
|
<CollapsibleContent className={cn(className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type QueueProps = ComponentProps<"div">;
|
export type QueueProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const Queue = ({ className, ...props }: QueueProps) => (
|
export const Queue = ({ className, ...props }: QueueProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
|
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,192 +1,192 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
import { reasoningPlugins } from "@/core/streamdown/plugins";
|
import { reasoningPlugins } from "@/core/streamdown/plugins";
|
||||||
import { Shimmer } from "./shimmer";
|
import { Shimmer } from "./shimmer";
|
||||||
|
|
||||||
type ReasoningContextValue = {
|
type ReasoningContextValue = {
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: (open: boolean) => void;
|
setIsOpen: (open: boolean) => void;
|
||||||
duration: number | undefined;
|
duration: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
||||||
|
|
||||||
export const useReasoning = () => {
|
export const useReasoning = () => {
|
||||||
const context = useContext(ReasoningContext);
|
const context = useContext(ReasoningContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("Reasoning components must be used within Reasoning");
|
throw new Error("Reasoning components must be used within Reasoning");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 1000;
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
const MS_IN_S = 1000;
|
const MS_IN_S = 1000;
|
||||||
|
|
||||||
export const Reasoning = memo(
|
export const Reasoning = memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
open,
|
open,
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
duration: durationProp,
|
duration: durationProp,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ReasoningProps) => {
|
}: ReasoningProps) => {
|
||||||
const [isOpen, setIsOpen] = useControllableState({
|
const [isOpen, setIsOpen] = useControllableState({
|
||||||
prop: open,
|
prop: open,
|
||||||
defaultProp: defaultOpen,
|
defaultProp: defaultOpen,
|
||||||
onChange: onOpenChange,
|
onChange: onOpenChange,
|
||||||
});
|
});
|
||||||
const [duration, setDuration] = useControllableState({
|
const [duration, setDuration] = useControllableState({
|
||||||
prop: durationProp,
|
prop: durationProp,
|
||||||
defaultProp: undefined,
|
defaultProp: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [hasAutoClosed, setHasAutoClosed] = useState(false);
|
const [hasAutoClosed, setHasAutoClosed] = useState(false);
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
|
|
||||||
// Track duration when streaming starts and ends
|
// Track duration when streaming starts and ends
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
if (startTime === null) {
|
if (startTime === null) {
|
||||||
setStartTime(Date.now());
|
setStartTime(Date.now());
|
||||||
}
|
}
|
||||||
} else if (startTime !== null) {
|
} else if (startTime !== null) {
|
||||||
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
|
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
|
||||||
setStartTime(null);
|
setStartTime(null);
|
||||||
}
|
}
|
||||||
}, [isStreaming, startTime, setDuration]);
|
}, [isStreaming, startTime, setDuration]);
|
||||||
|
|
||||||
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
||||||
// Add a small delay before closing to allow user to see the content
|
// Add a small delay before closing to allow user to see the content
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setHasAutoClosed(true);
|
setHasAutoClosed(true);
|
||||||
}, AUTO_CLOSE_DELAY);
|
}, AUTO_CLOSE_DELAY);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
|
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]);
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean) => {
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
setIsOpen(newOpen);
|
setIsOpen(newOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReasoningContext.Provider
|
<ReasoningContext.Provider
|
||||||
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
||||||
>
|
>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("not-prose mb-4", className)}
|
className={cn("not-prose mb-4", className)}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</ReasoningContext.Provider>
|
</ReasoningContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ReasoningTriggerProps = ComponentProps<
|
export type ReasoningTriggerProps = ComponentProps<
|
||||||
typeof CollapsibleTrigger
|
typeof CollapsibleTrigger
|
||||||
> & {
|
> & {
|
||||||
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
||||||
if (isStreaming || duration === 0) {
|
if (isStreaming || duration === 0) {
|
||||||
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
||||||
}
|
}
|
||||||
if (duration === undefined) {
|
if (duration === undefined) {
|
||||||
return <span>Thought for a few seconds</span>;
|
return <span>Thought for a few seconds</span>;
|
||||||
}
|
}
|
||||||
return <span>Thought for {duration} seconds</span>;
|
return <span>Thought for {duration} seconds</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReasoningTrigger = memo(
|
export const ReasoningTrigger = memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
getThinkingMessage = defaultGetThinkingMessage,
|
getThinkingMessage = defaultGetThinkingMessage,
|
||||||
...props
|
...props
|
||||||
}: ReasoningTriggerProps) => {
|
}: ReasoningTriggerProps) => {
|
||||||
const { isStreaming, isOpen, duration } = useReasoning();
|
const { isStreaming, isOpen, duration } = useReasoning();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<BrainIcon className="size-4" />
|
<BrainIcon className="size-4" />
|
||||||
{getThinkingMessage(isStreaming, duration)}
|
{getThinkingMessage(isStreaming, duration)}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0",
|
isOpen ? "rotate-180" : "rotate-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ReasoningContentProps = ComponentProps<
|
export type ReasoningContentProps = ComponentProps<
|
||||||
typeof CollapsibleContent
|
typeof CollapsibleContent
|
||||||
> & {
|
> & {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReasoningContent = memo(
|
export const ReasoningContent = memo(
|
||||||
({ className, children, ...props }: ReasoningContentProps) => (
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-4 text-sm",
|
"mt-4 text-sm",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{typeof children === "string" ? (
|
{typeof children === "string" ? (
|
||||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Reasoning.displayName = "Reasoning";
|
Reasoning.displayName = "Reasoning";
|
||||||
ReasoningTrigger.displayName = "ReasoningTrigger";
|
ReasoningTrigger.displayName = "ReasoningTrigger";
|
||||||
ReasoningContent.displayName = "ReasoningContent";
|
ReasoningContent.displayName = "ReasoningContent";
|
||||||
@@ -1,64 +1,64 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import {
|
import {
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
type ElementType,
|
type ElementType,
|
||||||
type JSX,
|
type JSX,
|
||||||
memo,
|
memo,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
export type TextShimmerProps = {
|
export type TextShimmerProps = {
|
||||||
children: string;
|
children: string;
|
||||||
as?: ElementType;
|
as?: ElementType;
|
||||||
className?: string;
|
className?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
spread?: number;
|
spread?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShimmerComponent = ({
|
const ShimmerComponent = ({
|
||||||
children,
|
children,
|
||||||
as: Component = "p",
|
as: Component = "p",
|
||||||
className,
|
className,
|
||||||
duration = 2,
|
duration = 2,
|
||||||
spread = 2,
|
spread = 2,
|
||||||
}: TextShimmerProps) => {
|
}: TextShimmerProps) => {
|
||||||
const MotionComponent = motion.create(
|
const MotionComponent = motion.create(
|
||||||
Component as keyof JSX.IntrinsicElements,
|
Component as keyof JSX.IntrinsicElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dynamicSpread = useMemo(
|
const dynamicSpread = useMemo(
|
||||||
() => (children?.length ?? 0) * spread,
|
() => (children?.length ?? 0) * spread,
|
||||||
[children, spread],
|
[children, spread],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MotionComponent
|
<MotionComponent
|
||||||
animate={{ backgroundPosition: "0% center" }}
|
animate={{ backgroundPosition: "0% center" }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
||||||
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
|
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
initial={{ backgroundPosition: "100% center" }}
|
initial={{ backgroundPosition: "100% center" }}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--spread": `${dynamicSpread}px`,
|
"--spread": `${dynamicSpread}px`,
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
|
"var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
transition={{
|
transition={{
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
duration,
|
duration,
|
||||||
ease: "linear",
|
ease: "linear",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MotionComponent>
|
</MotionComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Shimmer = memo(ShimmerComponent);
|
export const Shimmer = memo(ShimmerComponent);
|
||||||
@@ -1,77 +1,77 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type SourcesProps = ComponentProps<"div">;
|
export type SourcesProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("not-prose text-primary mb-4 text-xs", className)}
|
className={cn("not-prose text-primary mb-4 text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SourcesTrigger = ({
|
export const SourcesTrigger = ({
|
||||||
className,
|
className,
|
||||||
count,
|
count,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: SourcesTriggerProps) => (
|
}: SourcesTriggerProps) => (
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
className={cn("flex items-center gap-2", className)}
|
className={cn("flex items-center gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium">Used {count} sources</p>
|
<p className="font-medium">Used {count} sources</p>
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
export const SourcesContent = ({
|
export const SourcesContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: SourcesContentProps) => (
|
}: SourcesContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-3 flex w-fit flex-col gap-2",
|
"mt-3 flex w-fit flex-col gap-2",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SourceProps = ComponentProps<"a">;
|
export type SourceProps = ComponentProps<"a">;
|
||||||
|
|
||||||
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
href={href}
|
href={href}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<>
|
<>
|
||||||
<BookIcon className="h-4 w-4" />
|
<BookIcon className="h-4 w-4" />
|
||||||
<span className="block font-medium">{title}</span>
|
<span className="block font-medium">{title}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -1,80 +1,80 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Children, type ComponentProps } from "react";
|
import { Children, type ComponentProps } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const STAGGER_DELAY_MS = 60;
|
const STAGGER_DELAY_MS = 60;
|
||||||
const STAGGER_DELAY_MS_OFFSET = 250;
|
const STAGGER_DELAY_MS_OFFSET = 250;
|
||||||
|
|
||||||
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
||||||
|
|
||||||
export const Suggestions = ({
|
export const Suggestions = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: SuggestionsProps) => (
|
}: SuggestionsProps) => (
|
||||||
<ScrollArea className="overflow-x-auto whitespace-normal" {...props}>
|
<ScrollArea className="overflow-x-auto whitespace-normal" {...props}>
|
||||||
<div
|
<div
|
||||||
className={cn("flex w-full flex-wrap items-center gap-2", className)}
|
className={cn("flex w-full flex-wrap items-center gap-2", className)}
|
||||||
data-slot="suggestions-list"
|
data-slot="suggestions-list"
|
||||||
>
|
>
|
||||||
{Children.map(children, (child, index) =>
|
{Children.map(children, (child, index) =>
|
||||||
child != null ? (
|
child != null ? (
|
||||||
<span
|
<span
|
||||||
className="animate-fade-in-up max-w-full opacity-0"
|
className="animate-fade-in-up max-w-full opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`,
|
animationDelay: `${STAGGER_DELAY_MS_OFFSET + index * STAGGER_DELAY_MS}ms`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{child}
|
{child}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
child
|
child
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ScrollBar className="hidden" orientation="horizontal" />
|
<ScrollBar className="hidden" orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
||||||
suggestion: React.ReactNode;
|
suggestion: React.ReactNode;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Suggestion = ({
|
export const Suggestion = ({
|
||||||
suggestion,
|
suggestion,
|
||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "sm",
|
size = "sm",
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: SuggestionProps) => {
|
}: SuggestionProps) => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
onClick?.();
|
onClick?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground dark:bg-background h-auto max-w-full cursor-pointer rounded-full px-4 py-2 text-center text-xs font-normal whitespace-normal",
|
"text-muted-foreground dark:bg-background h-auto max-w-full cursor-pointer rounded-full px-4 py-2 text-center text-xs font-normal whitespace-normal",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
size={size}
|
size={size}
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon && <Icon className="size-4" />}
|
{Icon && <Icon className="size-4" />}
|
||||||
{children ?? suggestion}
|
{children ?? suggestion}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,87 +1,87 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
export type TaskItemFileProps = ComponentProps<"div">;
|
export type TaskItemFileProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const TaskItemFile = ({
|
export const TaskItemFile = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: TaskItemFileProps) => (
|
}: TaskItemFileProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
|
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TaskItemProps = ComponentProps<"div">;
|
export type TaskItemProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
||||||
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
|
<div className={cn("text-muted-foreground text-sm", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TaskProps = ComponentProps<typeof Collapsible>;
|
export type TaskProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
export const Task = ({
|
export const Task = ({
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: TaskProps) => (
|
}: TaskProps) => (
|
||||||
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
<Collapsible className={cn(className)} defaultOpen={defaultOpen} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TaskTrigger = ({
|
export const TaskTrigger = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
...props
|
...props
|
||||||
}: TaskTriggerProps) => (
|
}: TaskTriggerProps) => (
|
||||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
|
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
|
||||||
<SearchIcon className="size-4" />
|
<SearchIcon className="size-4" />
|
||||||
<p className="text-sm">{title}</p>
|
<p className="text-sm">{title}</p>
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
export const TaskContent = ({
|
export const TaskContent = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: TaskContentProps) => (
|
}: TaskContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
|
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
);
|
);
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { NodeToolbar, Position } from "@xyflow/react";
|
import { NodeToolbar, Position } from "@xyflow/react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
type ToolbarProps = ComponentProps<typeof NodeToolbar>;
|
type ToolbarProps = ComponentProps<typeof NodeToolbar>;
|
||||||
|
|
||||||
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
||||||
<NodeToolbar
|
<NodeToolbar
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
|
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,263 +1,263 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
export type WebPreviewContextValue = {
|
export type WebPreviewContextValue = {
|
||||||
url: string;
|
url: string;
|
||||||
setUrl: (url: string) => void;
|
setUrl: (url: string) => void;
|
||||||
consoleOpen: boolean;
|
consoleOpen: boolean;
|
||||||
setConsoleOpen: (open: boolean) => void;
|
setConsoleOpen: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
||||||
|
|
||||||
const useWebPreview = () => {
|
const useWebPreview = () => {
|
||||||
const context = useContext(WebPreviewContext);
|
const context = useContext(WebPreviewContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("WebPreview components must be used within a WebPreview");
|
throw new Error("WebPreview components must be used within a WebPreview");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebPreviewProps = ComponentProps<"div"> & {
|
export type WebPreviewProps = ComponentProps<"div"> & {
|
||||||
defaultUrl?: string;
|
defaultUrl?: string;
|
||||||
onUrlChange?: (url: string) => void;
|
onUrlChange?: (url: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WebPreview = ({
|
export const WebPreview = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
defaultUrl = "",
|
defaultUrl = "",
|
||||||
onUrlChange,
|
onUrlChange,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewProps) => {
|
}: WebPreviewProps) => {
|
||||||
const [url, setUrl] = useState(defaultUrl);
|
const [url, setUrl] = useState(defaultUrl);
|
||||||
const [consoleOpen, setConsoleOpen] = useState(false);
|
const [consoleOpen, setConsoleOpen] = useState(false);
|
||||||
|
|
||||||
const handleUrlChange = (newUrl: string) => {
|
const handleUrlChange = (newUrl: string) => {
|
||||||
setUrl(newUrl);
|
setUrl(newUrl);
|
||||||
onUrlChange?.(newUrl);
|
onUrlChange?.(newUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextValue: WebPreviewContextValue = {
|
const contextValue: WebPreviewContextValue = {
|
||||||
url,
|
url,
|
||||||
setUrl: handleUrlChange,
|
setUrl: handleUrlChange,
|
||||||
consoleOpen,
|
consoleOpen,
|
||||||
setConsoleOpen,
|
setConsoleOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebPreviewContext.Provider value={contextValue}>
|
<WebPreviewContext.Provider value={contextValue}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card flex size-full flex-col rounded-lg border",
|
"bg-card flex size-full flex-col rounded-lg border",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</WebPreviewContext.Provider>
|
</WebPreviewContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
export type WebPreviewNavigationProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const WebPreviewNavigation = ({
|
export const WebPreviewNavigation = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewNavigationProps) => (
|
}: WebPreviewNavigationProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center gap-1 border-b p-2", className)}
|
className={cn("flex items-center gap-1 border-b p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WebPreviewNavigationButton = ({
|
export const WebPreviewNavigationButton = ({
|
||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
tooltip,
|
tooltip,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewNavigationButtonProps) => (
|
}: WebPreviewNavigationButtonProps) => (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="hover:text-foreground h-8 w-8 p-0"
|
className="hover:text-foreground h-8 w-8 p-0"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{tooltip}</p>
|
<p>{tooltip}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
||||||
|
|
||||||
export const WebPreviewUrl = ({
|
export const WebPreviewUrl = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewUrlProps) => {
|
}: WebPreviewUrlProps) => {
|
||||||
const { url, setUrl } = useWebPreview();
|
const { url, setUrl } = useWebPreview();
|
||||||
const [inputValue, setInputValue] = useState(url);
|
const [inputValue, setInputValue] = useState(url);
|
||||||
|
|
||||||
// Sync input value with context URL when it changes externally
|
// Sync input value with context URL when it changes externally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInputValue(url);
|
setInputValue(url);
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
onChange?.(event);
|
onChange?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
setUrl(target.value);
|
setUrl(target.value);
|
||||||
}
|
}
|
||||||
onKeyDown?.(event);
|
onKeyDown?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
className="h-8 flex-1 text-sm"
|
className="h-8 flex-1 text-sm"
|
||||||
onChange={onChange ?? handleChange}
|
onChange={onChange ?? handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Enter URL..."
|
placeholder="Enter URL..."
|
||||||
value={value ?? inputValue}
|
value={value ?? inputValue}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
|
||||||
loading?: ReactNode;
|
loading?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WebPreviewBody = ({
|
export const WebPreviewBody = ({
|
||||||
className,
|
className,
|
||||||
loading,
|
loading,
|
||||||
src,
|
src,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewBodyProps) => {
|
}: WebPreviewBodyProps) => {
|
||||||
const { url } = useWebPreview();
|
const { url } = useWebPreview();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<iframe
|
<iframe
|
||||||
className={cn("size-full", className)}
|
className={cn("size-full", className)}
|
||||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
||||||
src={(src ?? url) || undefined}
|
src={(src ?? url) || undefined}
|
||||||
title="Preview"
|
title="Preview"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{loading}
|
{loading}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
|
||||||
logs?: Array<{
|
logs?: Array<{
|
||||||
level: "log" | "warn" | "error";
|
level: "log" | "warn" | "error";
|
||||||
message: string;
|
message: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WebPreviewConsole = ({
|
export const WebPreviewConsole = ({
|
||||||
className,
|
className,
|
||||||
logs = [],
|
logs = [],
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: WebPreviewConsoleProps) => {
|
}: WebPreviewConsoleProps) => {
|
||||||
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
|
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
|
||||||
onOpenChange={setConsoleOpen}
|
onOpenChange={setConsoleOpen}
|
||||||
open={consoleOpen}
|
open={consoleOpen}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
|
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Console
|
Console
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 transition-transform duration-200",
|
"h-4 w-4 transition-transform duration-200",
|
||||||
consoleOpen && "rotate-180",
|
consoleOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-4 pb-4",
|
"px-4 pb-4",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<p className="text-muted-foreground">No console output</p>
|
<p className="text-muted-foreground">No console output</p>
|
||||||
) : (
|
) : (
|
||||||
logs.map((log, index) => (
|
logs.map((log, index) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs",
|
"text-xs",
|
||||||
log.level === "error" && "text-destructive",
|
log.level === "error" && "text-destructive",
|
||||||
log.level === "warn" && "text-yellow-600",
|
log.level === "warn" && "text-yellow-600",
|
||||||
log.level === "log" && "text-foreground",
|
log.level === "log" && "text-foreground",
|
||||||
)}
|
)}
|
||||||
key={`${log.timestamp.getTime()}-${index}`}
|
key={`${log.timestamp.getTime()}-${index}`}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{log.timestamp.toLocaleTimeString()}
|
{log.timestamp.toLocaleTimeString()}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{log.message}
|
{log.message}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const year = useMemo(() => new Date().getFullYear(), []);
|
const year = useMemo(() => new Date().getFullYear(), []);
|
||||||
return (
|
return (
|
||||||
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center">
|
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center">
|
||||||
<hr className="from-border/0 to-border/0 m-0 h-px w-full border-none bg-linear-to-r via-white/20" />
|
<hr className="from-border/0 to-border/0 m-0 h-px w-full border-none bg-linear-to-r via-white/20" />
|
||||||
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
|
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
|
||||||
<p className="text-center font-serif text-lg md:text-xl">
|
<p className="text-center font-serif text-lg md:text-xl">
|
||||||
"Originated from Open Source, give back to Open Source."
|
"Originated from Open Source, give back to Open Source."
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
||||||
<p>Licensed under MIT License</p>
|
<p>Licensed under MIT License</p>
|
||||||
<p>© {year} CM 助手</p>
|
<p>© {year} CM 助手</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import type { Locale } from "@/core/i18n/locale";
|
import type { Locale } from "@/core/i18n/locale";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type HeaderProps = {
|
export type HeaderProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
homeURL?: string;
|
homeURL?: string;
|
||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Header({ className, homeURL }: HeaderProps) {
|
export function Header({ className, homeURL }: HeaderProps) {
|
||||||
const isExternalHome = !homeURL;
|
const isExternalHome = !homeURL;
|
||||||
const { locale: resolvedLocale, t } = useI18n();
|
const { locale: resolvedLocale, t } = useI18n();
|
||||||
const lang = resolvedLocale.substring(0, 2);
|
const lang = resolvedLocale.substring(0, 2);
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs",
|
"container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<a
|
<a
|
||||||
href={homeURL ?? "/"}
|
href={homeURL ?? "/"}
|
||||||
target={isExternalHome ? "_blank" : "_self"}
|
target={isExternalHome ? "_blank" : "_self"}
|
||||||
rel={isExternalHome ? "noopener noreferrer" : undefined}
|
rel={isExternalHome ? "noopener noreferrer" : undefined}
|
||||||
>
|
>
|
||||||
<h1 className="font-serif text-xl">CM 助手</h1>
|
<h1 className="font-serif text-xl">CM 助手</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
|
<nav className="mr-8 ml-auto flex items-center gap-8 text-sm font-medium">
|
||||||
<Link
|
<Link
|
||||||
href={`/${lang}/docs`}
|
href={`/${lang}/docs`}
|
||||||
className="text-secondary-foreground hover:text-foreground transition-colors"
|
className="text-secondary-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{t.home.docs}
|
{t.home.docs}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/blog/posts"
|
href="/blog/posts"
|
||||||
className="text-secondary-foreground hover:text-foreground transition-colors"
|
className="text-secondary-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{t.home.blog}
|
{t.home.blog}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
|
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,141 +1,136 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronRightIcon } from "lucide-react";
|
import { ChevronRightIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import deerMaskUrl from "@/assets/deer.svg";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
||||||
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
import Galaxy from "@/components/ui/galaxy";
|
||||||
import Galaxy from "@/components/ui/galaxy";
|
import { WordRotate } from "@/components/ui/word-rotate";
|
||||||
import { WordRotate } from "@/components/ui/word-rotate";
|
import { env } from "@/env";
|
||||||
import { env } from "@/env";
|
import { cn } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
export function Hero({ className }: { className?: string }) {
|
||||||
export function Hero({ className }: { className?: string }) {
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
"flex size-full flex-col items-center justify-center",
|
||||||
"flex size-full flex-col items-center justify-center",
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<div className="absolute inset-0 z-0 bg-black/40">
|
||||||
<div className="absolute inset-0 z-0 bg-black/40">
|
<Galaxy
|
||||||
<Galaxy
|
mouseRepulsion={false}
|
||||||
mouseRepulsion={false}
|
starSpeed={0.2}
|
||||||
starSpeed={0.2}
|
density={0.6}
|
||||||
density={0.6}
|
glowIntensity={0.35}
|
||||||
glowIntensity={0.35}
|
twinkleIntensity={0.3}
|
||||||
twinkleIntensity={0.3}
|
speed={0.5}
|
||||||
speed={0.5}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<FlickeringGrid
|
||||||
<FlickeringGrid
|
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
||||||
className="absolute inset-0 z-0 translate-y-8 mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
squareSize={4}
|
||||||
style={{
|
gridGap={4}
|
||||||
WebkitMaskImage: `url("${deerMaskUrl}")`,
|
color={"white"}
|
||||||
maskImage: `url("${deerMaskUrl}")`,
|
maxOpacity={0.3}
|
||||||
}}
|
flickerChance={0.25}
|
||||||
squareSize={4}
|
/>
|
||||||
gridGap={4}
|
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
|
||||||
color={"white"}
|
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
|
||||||
maxOpacity={0.3}
|
<WordRotate
|
||||||
flickerChance={0.25}
|
words={[
|
||||||
/>
|
"Deep Research",
|
||||||
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
|
"Collect Data",
|
||||||
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
|
"Analyze Data",
|
||||||
<WordRotate
|
"Generate Webpages",
|
||||||
words={[
|
"Vibe Coding",
|
||||||
"Deep Research",
|
"Generate Slides",
|
||||||
"Collect Data",
|
"Generate Images",
|
||||||
"Analyze Data",
|
"Generate Podcasts",
|
||||||
"Generate Webpages",
|
"Generate Videos",
|
||||||
"Vibe Coding",
|
"Generate Songs",
|
||||||
"Generate Slides",
|
"Organize Emails",
|
||||||
"Generate Images",
|
"Do Anything",
|
||||||
"Generate Podcasts",
|
"Learn Anything",
|
||||||
"Generate Videos",
|
]}
|
||||||
"Generate Songs",
|
/>{" "}
|
||||||
"Organize Emails",
|
<div>CM 助手</div>
|
||||||
"Do Anything",
|
</h1>
|
||||||
"Learn Anything",
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
||||||
]}
|
<a
|
||||||
/>{" "}
|
href="https://byteplus.com"
|
||||||
<div>CM 助手</div>
|
target="_blank"
|
||||||
</h1>
|
rel="noopener noreferrer"
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
>
|
||||||
<a
|
<div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm">
|
||||||
href="https://byteplus.com"
|
<span>In partnership with</span>
|
||||||
target="_blank"
|
<BytePlusIcon className="h-4" />
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
</a>
|
||||||
<div className="mt-4 flex size-fit items-center rounded-lg border border-white px-3 text-center text-shadow-sm">
|
)}
|
||||||
<span>In partnership with</span>
|
<p className="text-muted-foreground mt-8 scale-105 text-center text-2xl text-shadow-sm">
|
||||||
<BytePlusIcon className="h-4" />
|
An open-source SuperAgent harness that researches, codes, and creates.
|
||||||
</div>
|
With
|
||||||
</a>
|
<br />
|
||||||
)}
|
the help of sandboxes, memories, tools, skills and subagents, it
|
||||||
<p className="text-muted-foreground mt-8 scale-105 text-center text-2xl text-shadow-sm">
|
handles
|
||||||
An open-source SuperAgent harness that researches, codes, and creates.
|
<br />
|
||||||
With
|
different levels of tasks that could take minutes to hours.
|
||||||
<br />
|
</p>
|
||||||
the help of sandboxes, memories, tools, skills and subagents, it
|
<Link href="/workspace">
|
||||||
handles
|
<Button className="size-lg mt-8 scale-108" size="lg">
|
||||||
<br />
|
<span className="text-md">Get Started with 2.0</span>
|
||||||
different levels of tasks that could take minutes to hours.
|
<ChevronRightIcon className="size-4" />
|
||||||
</p>
|
</Button>
|
||||||
<Link href="/workspace">
|
</Link>
|
||||||
<Button className="size-lg mt-8 scale-108" size="lg">
|
</div>
|
||||||
<span className="text-md">Get Started with 2.0</span>
|
</div>
|
||||||
<ChevronRightIcon className="size-4" />
|
);
|
||||||
</Button>
|
}
|
||||||
</Link>
|
|
||||||
</div>
|
function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
</div>
|
return (
|
||||||
);
|
<svg
|
||||||
}
|
width={80}
|
||||||
|
height={24}
|
||||||
function BytePlusIcon(props: React.SVGProps<SVGSVGElement>) {
|
viewBox="0 0 120 24"
|
||||||
return (
|
fill="none"
|
||||||
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={80}
|
{...props}
|
||||||
height={24}
|
>
|
||||||
viewBox="0 0 120 24"
|
<path
|
||||||
fill="none"
|
d="M30.8027 3.64648H37.9333C38.8894 3.64648 39.7259 3.81911 40.4297 4.16435C41.1334 4.50959 41.6911 4.98761 42.0762 5.59843C42.4613 6.20924 42.6472 6.88644 42.6472 7.6566C42.6472 8.34708 42.4746 8.99773 42.1426 9.58198C41.7973 10.1795 41.3326 10.631 40.7483 10.9629C41.5583 11.3082 42.1691 11.8128 42.5808 12.49C42.9924 13.1672 43.1916 13.9108 43.1916 14.734C43.1916 15.5175 42.9924 16.208 42.5941 16.8188C42.1957 17.4296 41.638 17.9209 40.921 18.2661C40.2039 18.6246 39.3408 18.7973 38.3449 18.7973H30.8027V3.64648ZM37.8005 9.99361C38.4113 9.99361 38.9026 9.82099 39.2877 9.47575C39.6728 9.13051 39.8587 8.69232 39.8587 8.1479C39.8587 7.5902 39.6728 7.12546 39.301 6.76694C38.9292 6.40842 38.4379 6.22252 37.8138 6.22252H33.5248V9.99361H37.8005ZM38.1458 16.2345C38.7831 16.2345 39.301 16.0619 39.6993 15.7034C40.0977 15.3449 40.2969 14.8934 40.2969 14.349C40.2969 13.9772 40.2039 13.6319 40.018 13.3398C39.8321 13.0477 39.5798 12.8087 39.2479 12.636C38.9292 12.4634 38.5574 12.3705 38.1325 12.3705H33.5381V16.2212H38.1458V16.2345Z"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="currentColor"
|
||||||
{...props}
|
/>
|
||||||
>
|
<path
|
||||||
<path
|
d="M49.2392 18.2385L44.127 7.3236H46.7295L50.567 15.4899L54.0062 7.3236H57.6445V5.09281L60.2073 4.33594V7.3236H63.0887V9.75357H60.2073V15.304C60.2073 15.7023 60.3135 16.021 60.5259 16.2202C60.7384 16.4327 61.0438 16.5655 61.4422 16.6318C61.8405 16.6982 62.3982 16.7115 63.0887 16.6584V18.9556C61.7741 19.1016 60.7251 19.0618 59.9417 18.8626C59.1582 18.6502 58.574 18.2518 58.2022 17.6676C57.8304 17.0833 57.6445 16.2866 57.6445 15.2907V9.75357H55.5863L49.4782 24.0014H46.7959L49.2392 18.2385Z"
|
||||||
d="M30.8027 3.64648H37.9333C38.8894 3.64648 39.7259 3.81911 40.4297 4.16435C41.1334 4.50959 41.6911 4.98761 42.0762 5.59843C42.4613 6.20924 42.6472 6.88644 42.6472 7.6566C42.6472 8.34708 42.4746 8.99773 42.1426 9.58198C41.7973 10.1795 41.3326 10.631 40.7483 10.9629C41.5583 11.3082 42.1691 11.8128 42.5808 12.49C42.9924 13.1672 43.1916 13.9108 43.1916 14.734C43.1916 15.5175 42.9924 16.208 42.5941 16.8188C42.1957 17.4296 41.638 17.9209 40.921 18.2661C40.2039 18.6246 39.3408 18.7973 38.3449 18.7973H30.8027V3.64648ZM37.8005 9.99361C38.4113 9.99361 38.9026 9.82099 39.2877 9.47575C39.6728 9.13051 39.8587 8.69232 39.8587 8.1479C39.8587 7.5902 39.6728 7.12546 39.301 6.76694C38.9292 6.40842 38.4379 6.22252 37.8138 6.22252H33.5248V9.99361H37.8005ZM38.1458 16.2345C38.7831 16.2345 39.301 16.0619 39.6993 15.7034C40.0977 15.3449 40.2969 14.8934 40.2969 14.349C40.2969 13.9772 40.2039 13.6319 40.018 13.3398C39.8321 13.0477 39.5798 12.8087 39.2479 12.636C38.9292 12.4634 38.5574 12.3705 38.1325 12.3705H33.5381V16.2212H38.1458V16.2345Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M70.2461 19.1147C69.1572 19.1147 68.1746 18.8491 67.2849 18.318C66.3953 17.7869 65.6782 17.0565 65.1604 16.1138C64.6425 15.1843 64.377 14.1485 64.377 13.0066C64.377 11.8912 64.6425 10.882 65.1737 9.97908C65.7048 9.06286 66.4218 8.34582 67.3248 7.82796C68.2277 7.29682 69.2236 7.03125 70.2992 7.03125C71.6536 7.03125 72.7822 7.33666 73.6719 7.93419C74.5616 8.53172 75.2122 9.36827 75.584 10.4438C75.9691 11.5194 76.0753 12.7676 75.916 14.1751L67.0061 14.1884C67.1389 14.7062 67.3779 15.1577 67.7364 15.5295C68.0949 15.9013 68.5066 16.1802 68.958 16.366C69.4095 16.5519 69.8477 16.6316 70.2461 16.6316C70.6975 16.6316 71.1092 16.5918 71.5075 16.4988C71.8926 16.4059 72.2378 16.2731 72.5432 16.0739C72.8486 15.8747 73.1142 15.649 73.3665 15.3967L73.4727 15.2639L75.5043 16.6051C74.9998 17.4416 74.296 18.0657 73.4063 18.4773C72.5167 18.9022 71.4677 19.1147 70.2461 19.1147ZM73.486 11.9576C73.3665 11.4663 73.1673 11.0281 72.8619 10.6563C72.5565 10.2845 72.1847 9.99236 71.7332 9.79318C71.2818 9.594 70.8037 9.48778 70.2992 9.48778C69.8211 9.48778 69.3564 9.594 68.8916 9.79318C68.4269 9.99236 68.0285 10.2845 67.6833 10.6563C67.3381 11.0281 67.1256 11.4663 67.0459 11.9576H73.486Z"
|
||||||
d="M49.2392 18.2385L44.127 7.3236H46.7295L50.567 15.4899L54.0062 7.3236H57.6445V5.09281L60.2073 4.33594V7.3236H63.0887V9.75357H60.2073V15.304C60.2073 15.7023 60.3135 16.021 60.5259 16.2202C60.7384 16.4327 61.0438 16.5655 61.4422 16.6318C61.8405 16.6982 62.3982 16.7115 63.0887 16.6584V18.9556C61.7741 19.1016 60.7251 19.0618 59.9417 18.8626C59.1582 18.6502 58.574 18.2518 58.2022 17.6676C57.8304 17.0833 57.6445 16.2866 57.6445 15.2907V9.75357H55.5863L49.4782 24.0014H46.7959L49.2392 18.2385Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M78 3.64648H84.3206C86.0733 3.64648 87.4278 4.0714 88.3838 4.92122C89.3399 5.77105 89.8179 6.93956 89.8179 8.42675C89.8179 9.39608 89.6054 10.2459 89.1805 10.9762C88.7556 11.7065 88.1315 12.2642 87.3215 12.6493C86.5115 13.0344 85.5156 13.2336 84.3471 13.2336H80.7354V18.7973H78V3.64648ZM84.3737 10.7505C84.9314 10.7505 85.4227 10.6575 85.8078 10.4849C86.2061 10.3123 86.5115 10.0467 86.724 9.70149C86.9364 9.35625 87.0427 8.93133 87.0427 8.42675C87.0427 7.6566 86.8169 7.08562 86.3655 6.71382C85.914 6.34202 85.2501 6.15612 84.387 6.15612H80.7487V10.7505H84.3737Z"
|
||||||
d="M70.2461 19.1147C69.1572 19.1147 68.1746 18.8491 67.2849 18.318C66.3953 17.7869 65.6782 17.0565 65.1604 16.1138C64.6425 15.1843 64.377 14.1485 64.377 13.0066C64.377 11.8912 64.6425 10.882 65.1737 9.97908C65.7048 9.06286 66.4218 8.34582 67.3248 7.82796C68.2277 7.29682 69.2236 7.03125 70.2992 7.03125C71.6536 7.03125 72.7822 7.33666 73.6719 7.93419C74.5616 8.53172 75.2122 9.36827 75.584 10.4438C75.9691 11.5194 76.0753 12.7676 75.916 14.1751L67.0061 14.1884C67.1389 14.7062 67.3779 15.1577 67.7364 15.5295C68.0949 15.9013 68.5066 16.1802 68.958 16.366C69.4095 16.5519 69.8477 16.6316 70.2461 16.6316C70.6975 16.6316 71.1092 16.5918 71.5075 16.4988C71.8926 16.4059 72.2378 16.2731 72.5432 16.0739C72.8486 15.8747 73.1142 15.649 73.3665 15.3967L73.4727 15.2639L75.5043 16.6051C74.9998 17.4416 74.296 18.0657 73.4063 18.4773C72.5167 18.9022 71.4677 19.1147 70.2461 19.1147ZM73.486 11.9576C73.3665 11.4663 73.1673 11.0281 72.8619 10.6563C72.5565 10.2845 72.1847 9.99236 71.7332 9.79318C71.2818 9.594 70.8037 9.48778 70.2992 9.48778C69.8211 9.48778 69.3564 9.594 68.8916 9.79318C68.4269 9.99236 68.0285 10.2845 67.6833 10.6563C67.3381 11.0281 67.1256 11.4663 67.0459 11.9576H73.486Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z"
|
||||||
d="M78 3.64648H84.3206C86.0733 3.64648 87.4278 4.0714 88.3838 4.92122C89.3399 5.77105 89.8179 6.93956 89.8179 8.42675C89.8179 9.39608 89.6054 10.2459 89.1805 10.9762C88.7556 11.7065 88.1315 12.2642 87.3215 12.6493C86.5115 13.0344 85.5156 13.2336 84.3471 13.2336H80.7354V18.7973H78V3.64648ZM84.3737 10.7505C84.9314 10.7505 85.4227 10.6575 85.8078 10.4849C86.2061 10.3123 86.5115 10.0467 86.724 9.70149C86.9364 9.35625 87.0427 8.93133 87.0427 8.42675C87.0427 7.6566 86.8169 7.08562 86.3655 6.71382C85.914 6.34202 85.2501 6.15612 84.387 6.15612H80.7487V10.7505H84.3737Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M101.982 19.1155C101.158 19.1155 100.415 18.9164 99.764 18.5313C99.1134 18.1462 98.5955 17.6018 98.2237 16.9113C97.8519 16.2208 97.666 15.4507 97.666 14.6008V7.32422H100.282V13.6979C100.282 14.2822 100.375 14.7735 100.574 15.1984C100.773 15.6233 101.052 15.9553 101.424 16.1943C101.796 16.4333 102.247 16.5528 102.765 16.5528C103.256 16.5528 103.708 16.4333 104.106 16.181C104.504 15.942 104.836 15.5835 105.075 15.1187C105.314 14.654 105.434 14.1228 105.434 13.512V7.32422H108.076V18.8101H105.527V17.3628C105.155 17.947 104.651 18.3719 104.04 18.6774C103.442 18.9562 102.752 19.1155 101.982 19.1155Z"
|
||||||
d="M92.168 3.36719H94.8104V18.8101H92.168V3.36719Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M115.287 19.1163C114.424 19.1163 113.535 18.9304 112.645 18.5586C111.755 18.1868 111.012 17.6424 110.414 16.9386L112.1 15.5045C112.525 15.8763 112.95 16.1817 113.375 16.4075C113.813 16.6332 114.358 16.766 115.048 16.8058C115.752 16.8457 116.35 16.766 116.828 16.5535C117.306 16.3411 117.558 16.0357 117.584 15.6107C117.598 15.3186 117.518 15.0929 117.345 14.9335C117.173 14.7742 116.907 14.6281 116.535 14.5219C116.164 14.4157 115.593 14.2962 114.809 14.1634L114.265 14.0572C113.123 13.8713 112.246 13.5127 111.622 12.9949C110.998 12.477 110.693 11.7334 110.693 10.7906C110.693 10.0072 110.905 9.34329 111.317 8.77231C111.742 8.21461 112.286 7.7897 112.99 7.51085C113.681 7.232 114.451 7.08594 115.314 7.08594C116.19 7.08594 117.053 7.24528 117.863 7.56396C118.687 7.88265 119.364 8.40051 119.882 9.10427L118.209 10.5118C117.903 10.1267 117.505 9.83459 117 9.63541C116.496 9.43624 115.951 9.34329 115.354 9.34329C114.969 9.34329 114.597 9.38312 114.252 9.47607C113.906 9.56902 113.614 9.70181 113.388 9.90098C113.163 10.1002 113.043 10.3392 113.043 10.6446C113.043 10.9102 113.123 11.1093 113.269 11.2554C113.415 11.4015 113.694 11.5342 114.079 11.6405C114.464 11.7467 115.128 11.8928 116.044 12.0654H116.084C117.226 12.2778 118.169 12.6762 118.899 13.2339C119.629 13.7916 120.001 14.5617 120.001 15.5045C120.001 16.2348 119.775 16.8855 119.324 17.4299C118.872 17.9743 118.288 18.3992 117.558 18.6914C116.841 18.9702 116.084 19.1163 115.287 19.1163Z"
|
||||||
d="M101.982 19.1155C101.158 19.1155 100.415 18.9164 99.764 18.5313C99.1134 18.1462 98.5955 17.6018 98.2237 16.9113C97.8519 16.2208 97.666 15.4507 97.666 14.6008V7.32422H100.282V13.6979C100.282 14.2822 100.375 14.7735 100.574 15.1984C100.773 15.6233 101.052 15.9553 101.424 16.1943C101.796 16.4333 102.247 16.5528 102.765 16.5528C103.256 16.5528 103.708 16.4333 104.106 16.181C104.504 15.942 104.836 15.5835 105.075 15.1187C105.314 14.654 105.434 14.1228 105.434 13.512V7.32422H108.076V18.8101H105.527V17.3628C105.155 17.947 104.651 18.3719 104.04 18.6774C103.442 18.9562 102.752 19.1155 101.982 19.1155Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M14.7392 9.80759C14.5931 9.92709 14.3806 9.83414 14.3806 9.63497V8.58596V2.23883C14.3806 2.05293 14.1549 1.9467 14.0221 2.06621L5.17862 9.56857C5.03256 9.68808 4.8201 9.59513 4.8201 9.39595V4.27044C4.8201 4.12438 4.70059 4.00487 4.55453 4.00487H0.26557C0.119507 4.00487 0 4.12438 0 4.27044V20.3242C0 20.5101 0.225735 20.6163 0.35852 20.4968L9.18873 12.9944C9.33479 12.8749 9.54725 12.9679 9.54725 13.1671V20.5765C9.54725 20.7624 9.77299 20.8686 9.90577 20.7491L18.736 13.2467C18.882 13.1272 19.0945 13.2202 19.0945 13.4193V18.5449C19.0945 18.6909 19.214 18.8104 19.3601 18.8104H23.649C23.7951 18.8104 23.9146 18.6909 23.9146 18.5449V2.47784C23.9146 2.29194 23.6889 2.18572 23.5561 2.30522L14.7392 9.80759Z"
|
||||||
d="M115.287 19.1163C114.424 19.1163 113.535 18.9304 112.645 18.5586C111.755 18.1868 111.012 17.6424 110.414 16.9386L112.1 15.5045C112.525 15.8763 112.95 16.1817 113.375 16.4075C113.813 16.6332 114.358 16.766 115.048 16.8058C115.752 16.8457 116.35 16.766 116.828 16.5535C117.306 16.3411 117.558 16.0357 117.584 15.6107C117.598 15.3186 117.518 15.0929 117.345 14.9335C117.173 14.7742 116.907 14.6281 116.535 14.5219C116.164 14.4157 115.593 14.2962 114.809 14.1634L114.265 14.0572C113.123 13.8713 112.246 13.5127 111.622 12.9949C110.998 12.477 110.693 11.7334 110.693 10.7906C110.693 10.0072 110.905 9.34329 111.317 8.77231C111.742 8.21461 112.286 7.7897 112.99 7.51085C113.681 7.232 114.451 7.08594 115.314 7.08594C116.19 7.08594 117.053 7.24528 117.863 7.56396C118.687 7.88265 119.364 8.40051 119.882 9.10427L118.209 10.5118C117.903 10.1267 117.505 9.83459 117 9.63541C116.496 9.43624 115.951 9.34329 115.354 9.34329C114.969 9.34329 114.597 9.38312 114.252 9.47607C113.906 9.56902 113.614 9.70181 113.388 9.90098C113.163 10.1002 113.043 10.3392 113.043 10.6446C113.043 10.9102 113.123 11.1093 113.269 11.2554C113.415 11.4015 113.694 11.5342 114.079 11.6405C114.464 11.7467 115.128 11.8928 116.044 12.0654H116.084C117.226 12.2778 118.169 12.6762 118.899 13.2339C119.629 13.7916 120.001 14.5617 120.001 15.5045C120.001 16.2348 119.775 16.8855 119.324 17.4299C118.872 17.9743 118.288 18.3992 117.558 18.6914C116.841 18.9702 116.084 19.1163 115.287 19.1163Z"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
/>
|
||||||
/>
|
</svg>
|
||||||
<path
|
);
|
||||||
d="M14.7392 9.80759C14.5931 9.92709 14.3806 9.83414 14.3806 9.63497V8.58596V2.23883C14.3806 2.05293 14.1549 1.9467 14.0221 2.06621L5.17862 9.56857C5.03256 9.68808 4.8201 9.59513 4.8201 9.39595V4.27044C4.8201 4.12438 4.70059 4.00487 4.55453 4.00487H0.26557C0.119507 4.00487 0 4.12438 0 4.27044V20.3242C0 20.5101 0.225735 20.6163 0.35852 20.4968L9.18873 12.9944C9.33479 12.8749 9.54725 12.9679 9.54725 13.1671V20.5765C9.54725 20.7624 9.77299 20.8686 9.90577 20.7491L18.736 13.2467C18.882 13.1272 19.0945 13.2202 19.0945 13.4193V18.5449C19.0945 18.6909 19.214 18.8104 19.3601 18.8104H23.649C23.7951 18.8104 23.9146 18.6909 23.9146 18.5449V2.47784C23.9146 2.29194 23.6889 2.18572 23.5561 2.30522L14.7392 9.80759Z"
|
}
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,163 +1,163 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog";
|
import { getBlogRoute, normalizeTagSlug, type BlogPost } from "@/core/blog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type PostListProps = {
|
type PostListProps = {
|
||||||
description?: string;
|
description?: string;
|
||||||
posts: BlogPost[];
|
posts: BlogPost[];
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PostMetaProps = {
|
type PostMetaProps = {
|
||||||
currentLang?: string;
|
currentLang?: string;
|
||||||
date?: string | null;
|
date?: string | null;
|
||||||
languages?: string[];
|
languages?: string[];
|
||||||
pathname?: string;
|
pathname?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDate(date?: string): string | null {
|
function formatDate(date?: string): string | null {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = new Date(date);
|
const value = new Date(date);
|
||||||
if (Number.isNaN(value.getTime())) {
|
if (Number.isNaN(value.getTime())) {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostMeta({
|
export function PostMeta({
|
||||||
currentLang,
|
currentLang,
|
||||||
date,
|
date,
|
||||||
languages,
|
languages,
|
||||||
pathname,
|
pathname,
|
||||||
}: PostMetaProps) {
|
}: PostMetaProps) {
|
||||||
const formattedDate = formatDate(date ?? undefined);
|
const formattedDate = formatDate(date ?? undefined);
|
||||||
const availableLanguages = Array.isArray(languages)
|
const availableLanguages = Array.isArray(languages)
|
||||||
? languages.filter((lang): lang is string => typeof lang === "string")
|
? languages.filter((lang): lang is string => typeof lang === "string")
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (!formattedDate && availableLanguages.length <= 1) {
|
if (!formattedDate && availableLanguages.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-8 text-sm">
|
<div className="flex flex-wrap items-center gap-8 text-sm">
|
||||||
{formattedDate ? (
|
{formattedDate ? (
|
||||||
<p className="text-muted-foreground">{formattedDate}</p>
|
<p className="text-muted-foreground">{formattedDate}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{pathname && availableLanguages.length > 1 ? (
|
{pathname && availableLanguages.length > 1 ? (
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<span className="text-secondary-foreground text-sm">Language:</span>
|
<span className="text-secondary-foreground text-sm">Language:</span>
|
||||||
{availableLanguages.map((lang) => {
|
{availableLanguages.map((lang) => {
|
||||||
const isActive = lang === currentLang;
|
const isActive = lang === currentLang;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={lang}
|
key={lang}
|
||||||
href={`${pathname}?lang=${lang}`}
|
href={`${pathname}?lang=${lang}`}
|
||||||
className={
|
className={
|
||||||
isActive
|
isActive
|
||||||
? "text-foreground text-sm font-medium"
|
? "text-foreground text-sm font-medium"
|
||||||
: "text-muted-foreground hover:text-foreground text-sm transition-colors"
|
: "text-muted-foreground hover:text-foreground text-sm transition-colors"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{lang.toUpperCase()}
|
{lang.toUpperCase()}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostTags({
|
export function PostTags({
|
||||||
tags,
|
tags,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
tags?: unknown;
|
tags?: unknown;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!Array.isArray(tags)) {
|
if (!Array.isArray(tags)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validTags = tags.filter(
|
const validTags = tags.filter(
|
||||||
(tag): tag is string => typeof tag === "string" && tag.length > 0,
|
(tag): tag is string => typeof tag === "string" && tag.length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validTags.length === 0) {
|
if (validTags.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-wrap items-center gap-3", className)}>
|
<div className={cn("flex flex-wrap items-center gap-3", className)}>
|
||||||
<span className="text-secondary-foreground text-sm">Tags:</span>
|
<span className="text-secondary-foreground text-sm">Tags:</span>
|
||||||
{validTags.map((tag) => (
|
{validTags.map((tag) => (
|
||||||
<Link
|
<Link
|
||||||
key={tag}
|
key={tag}
|
||||||
href={`/blog/tags/${normalizeTagSlug(tag)}`}
|
href={`/blog/tags/${normalizeTagSlug(tag)}`}
|
||||||
className="border-border text-secondary-foreground hover:text-foreground rounded-xl border px-2 py-1 text-sm transition-colors"
|
className="border-border text-secondary-foreground hover:text-foreground rounded-xl border px-2 py-1 text-sm transition-colors"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostList({ description, posts, title }: PostListProps) {
|
export function PostList({ description, posts, title }: PostListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6">
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-6">
|
||||||
<header className="space-y-4">
|
<header className="space-y-4">
|
||||||
<h2 className="text-foreground text-4xl font-semibold tracking-tight">
|
<h2 className="text-foreground text-4xl font-semibold tracking-tight">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{description ? (
|
{description ? (
|
||||||
<p className="text-secondary-foreground">{description}</p>
|
<p className="text-secondary-foreground">{description}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{posts.map((post) => {
|
{posts.map((post) => {
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
key={post.slug.join("/")}
|
key={post.slug.join("/")}
|
||||||
className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0"
|
className="border-border space-y-5 border-b pb-12 last:border-b-0 last:pb-0"
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PostMeta
|
<PostMeta
|
||||||
currentLang={post.lang}
|
currentLang={post.lang}
|
||||||
date={post.metadata.date}
|
date={post.metadata.date}
|
||||||
languages={post.languages}
|
languages={post.languages}
|
||||||
pathname={getBlogRoute(post.slug)}
|
pathname={getBlogRoute(post.slug)}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
href={getBlogRoute(post.slug)}
|
href={getBlogRoute(post.slug)}
|
||||||
className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors"
|
className="text-foreground hover:text-primary block text-2xl font-semibold tracking-tight transition-colors"
|
||||||
>
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{post.metadata.description ? (
|
{post.metadata.description ? (
|
||||||
<p className="text-secondary-foreground leading-10">
|
<p className="text-secondary-foreground leading-10">
|
||||||
{post.metadata.description}
|
{post.metadata.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<PostTags tags={post.metadata.tags} />
|
<PostTags tags={post.metadata.tags} />
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function Section({
|
export function Section({
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
subtitle?: React.ReactNode;
|
subtitle?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className={cn("mx-auto flex flex-col py-16", className)}>
|
<section className={cn("mx-auto flex flex-col py-16", className)}>
|
||||||
<header className="flex flex-col items-center justify-between">
|
<header className="flex flex-col items-center justify-between">
|
||||||
<div className="mb-4 bg-linear-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-center text-5xl font-bold text-transparent">
|
<div className="mb-4 bg-linear-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-center text-5xl font-bold text-transparent">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<div className="text-muted-foreground text-center text-xl">
|
<div className="text-muted-foreground text-center text-xl">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<main className="mt-4">{children}</main>
|
<main className="mt-4">{children}</main>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,99 +1,99 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { pathOfThread } from "@/core/threads/utils";
|
import { pathOfThread } from "@/core/threads/utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Section } from "../section";
|
import { Section } from "../section";
|
||||||
|
|
||||||
export function CaseStudySection({ className }: { className?: string }) {
|
export function CaseStudySection({ className }: { className?: string }) {
|
||||||
const caseStudies = [
|
const caseStudies = [
|
||||||
{
|
{
|
||||||
threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990",
|
threadId: "7cfa5f8f-a2f8-47ad-acbd-da7137baf990",
|
||||||
title: "Forecast 2026 Agent Trends and Opportunities",
|
title: "Forecast 2026 Agent Trends and Opportunities",
|
||||||
description:
|
description:
|
||||||
"Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.",
|
"Create a webpage with a Deep Research report forecasting the agent technology trends and opportunities in 2026.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb",
|
threadId: "4f3e55ee-f853-43db-bfb3-7d1a411f03cb",
|
||||||
title: 'Generate a Video Based On the Novel "Pride and Prejudice"',
|
title: 'Generate a Video Based On the Novel "Pride and Prejudice"',
|
||||||
description:
|
description:
|
||||||
'Search the specific scene from the novel "Pride and Prejudice", then generate a video as well as a reference image based on the scenes.',
|
'Search the specific scene from the novel "Pride and Prejudice", then generate a video as well as a reference image based on the scenes.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: "21cfea46-34bd-4aa6-9e1f-3009452fbeb9",
|
threadId: "21cfea46-34bd-4aa6-9e1f-3009452fbeb9",
|
||||||
title: "Doraemon Explains the MOE Architecture",
|
title: "Doraemon Explains the MOE Architecture",
|
||||||
description:
|
description:
|
||||||
"Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.",
|
"Generate a Doraemon comic strip explaining the MOE architecture to the teenagers who are interested in AI.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: "ad76c455-5bf9-4335-8517-fc03834ab828",
|
threadId: "ad76c455-5bf9-4335-8517-fc03834ab828",
|
||||||
title: "An Exploratory Data Analysis of the Titanic Dataset",
|
title: "An Exploratory Data Analysis of the Titanic Dataset",
|
||||||
description:
|
description:
|
||||||
"Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.",
|
"Explore the Titanic dataset and identify the key factors that influenced survival rates with visualizations and insights.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98",
|
threadId: "d3e5adaf-084c-4dd5-9d29-94f1d6bccd98",
|
||||||
title: "Watch Y Combinator's Video then Conduct a Deep Research",
|
title: "Watch Y Combinator's Video then Conduct a Deep Research",
|
||||||
description:
|
description:
|
||||||
"Watch the given Y Combinator's YouTube video and conduct a deep research on the YC's tips for technical startup founders.",
|
"Watch the given Y Combinator's YouTube video and conduct a deep research on the YC's tips for technical startup founders.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threadId: "3823e443-4e2b-4679-b496-a9506eae462b",
|
threadId: "3823e443-4e2b-4679-b496-a9506eae462b",
|
||||||
title: "Collect and Summarize Dr. Fei Fei Li's Podcasts",
|
title: "Collect and Summarize Dr. Fei Fei Li's Podcasts",
|
||||||
description:
|
description:
|
||||||
"Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.",
|
"Collect all the podcast appearances of Dr. Fei Fei Li in the last 6 months, then summarize them into a comprehensive report.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
className={className}
|
className={className}
|
||||||
title="Case Studies"
|
title="Case Studies"
|
||||||
subtitle="CM 助手"
|
subtitle="CM 助手"
|
||||||
>
|
>
|
||||||
<div className="container-md mt-8 grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-20 lg:grid-cols-3">
|
<div className="container-md mt-8 grid grid-cols-1 gap-4 px-4 md:grid-cols-2 md:px-20 lg:grid-cols-3">
|
||||||
{caseStudies.map((caseStudy) => (
|
{caseStudies.map((caseStudy) => (
|
||||||
<Link
|
<Link
|
||||||
key={caseStudy.title}
|
key={caseStudy.title}
|
||||||
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
|
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Card className="group/card relative h-64 overflow-hidden">
|
<Card className="group/card relative h-64 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat transition-all duration-300 group-hover/card:scale-110 group-hover/card:brightness-90"
|
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat transition-all duration-300 group-hover/card:scale-110 group-hover/card:brightness-90"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`,
|
backgroundImage: `url(/images/${caseStudy.threadId}.jpg)`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center",
|
"flex h-full w-full translate-y-[calc(100%-60px)] flex-col items-center",
|
||||||
"transition-all duration-300",
|
"transition-all duration-300",
|
||||||
"group-hover/card:translate-y-[calc(100%-128px)]",
|
"group-hover/card:translate-y-[calc(100%-128px)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex w-full flex-col p-4"
|
className="flex w-full flex-col p-4"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)",
|
"linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="flex h-14 items-center text-xl font-bold text-shadow-black">
|
<h3 className="flex h-14 items-center text-xl font-bold text-shadow-black">
|
||||||
{caseStudy.title}
|
{caseStudy.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black">
|
<p className="box-shadow-black overflow-hidden text-sm text-white/85 text-shadow-black">
|
||||||
{caseStudy.description}
|
{caseStudy.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AuroraText } from "@/components/ui/aurora-text";
|
import { AuroraText } from "@/components/ui/aurora-text";
|
||||||
|
|
||||||
import { Section } from "../section";
|
import { Section } from "../section";
|
||||||
|
|
||||||
export function CommunitySection() {
|
export function CommunitySection() {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
title={
|
title={
|
||||||
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
|
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
|
||||||
Join the Community
|
Join the Community
|
||||||
</AuroraText>
|
</AuroraText>
|
||||||
}
|
}
|
||||||
subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts."
|
subtitle="Contribute brilliant ideas to shape the future of CM Assistant. Collaborate, innovate, and make impacts."
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,126 +1,126 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnimatedSpan,
|
AnimatedSpan,
|
||||||
Terminal,
|
Terminal,
|
||||||
TypingAnimation,
|
TypingAnimation,
|
||||||
} from "@/components/ui/terminal";
|
} from "@/components/ui/terminal";
|
||||||
|
|
||||||
import { Section } from "../section";
|
import { Section } from "../section";
|
||||||
|
|
||||||
export function SandboxSection({ className }: { className?: string }) {
|
export function SandboxSection({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
className={className}
|
className={className}
|
||||||
title="Agent Runtime Environment"
|
title="Agent Runtime Environment"
|
||||||
subtitle={
|
subtitle={
|
||||||
<p>
|
<p>
|
||||||
We give CM Assistant a "computer", which can execute commands,
|
We give CM Assistant a "computer", which can execute commands,
|
||||||
manage files, and run long tasks — all in a secure Docker-based
|
manage files, and run long tasks — all in a secure Docker-based
|
||||||
sandbox
|
sandbox
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
|
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
|
||||||
{/* Left: Terminal */}
|
{/* Left: Terminal */}
|
||||||
<div className="w-full flex-1">
|
<div className="w-full flex-1">
|
||||||
<Terminal className="h-[360px] w-full">
|
<Terminal className="h-[360px] w-full">
|
||||||
{/* Scene 1: Build a Game */}
|
{/* Scene 1: Build a Game */}
|
||||||
<TypingAnimation>$ cat requirements.txt</TypingAnimation>
|
<TypingAnimation>$ cat requirements.txt</TypingAnimation>
|
||||||
<AnimatedSpan delay={800} className="text-zinc-400">
|
<AnimatedSpan delay={800} className="text-zinc-400">
|
||||||
pygame==2.5.0
|
pygame==2.5.0
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
|
|
||||||
<TypingAnimation delay={1200}>
|
<TypingAnimation delay={1200}>
|
||||||
$ pip install -r requirements.txt
|
$ pip install -r requirements.txt
|
||||||
</TypingAnimation>
|
</TypingAnimation>
|
||||||
<AnimatedSpan delay={2000} className="text-green-500">
|
<AnimatedSpan delay={2000} className="text-green-500">
|
||||||
[ok] Installed pygame
|
[ok] Installed pygame
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
|
|
||||||
<TypingAnimation delay={2400}>
|
<TypingAnimation delay={2400}>
|
||||||
$ write game.py --lines 156
|
$ write game.py --lines 156
|
||||||
</TypingAnimation>
|
</TypingAnimation>
|
||||||
<AnimatedSpan delay={3200} className="text-blue-500">
|
<AnimatedSpan delay={3200} className="text-blue-500">
|
||||||
[ok] Written 156 lines
|
[ok] Written 156 lines
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
|
|
||||||
<TypingAnimation delay={3600}>
|
<TypingAnimation delay={3600}>
|
||||||
$ python game.py --test
|
$ python game.py --test
|
||||||
</TypingAnimation>
|
</TypingAnimation>
|
||||||
<AnimatedSpan delay={4200} className="text-green-500">
|
<AnimatedSpan delay={4200} className="text-green-500">
|
||||||
[ok] All sprites loaded
|
[ok] All sprites loaded
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
<AnimatedSpan delay={4500} className="text-green-500">
|
<AnimatedSpan delay={4500} className="text-green-500">
|
||||||
[ok] Physics engine OK
|
[ok] Physics engine OK
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
<AnimatedSpan delay={4800} className="text-green-500">
|
<AnimatedSpan delay={4800} className="text-green-500">
|
||||||
[ok] 60 FPS stable
|
[ok] 60 FPS stable
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
|
|
||||||
{/* Scene 2: Data Analysis */}
|
{/* Scene 2: Data Analysis */}
|
||||||
<TypingAnimation delay={5400}>
|
<TypingAnimation delay={5400}>
|
||||||
$ curl -O sales-2024.csv
|
$ curl -O sales-2024.csv
|
||||||
</TypingAnimation>
|
</TypingAnimation>
|
||||||
<AnimatedSpan delay={6200} className="text-zinc-400">
|
<AnimatedSpan delay={6200} className="text-zinc-400">
|
||||||
Downloaded 12.4 MB
|
Downloaded 12.4 MB
|
||||||
</AnimatedSpan>
|
</AnimatedSpan>
|
||||||
</Terminal>
|
</Terminal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Description */}
|
{/* Right: Description */}
|
||||||
<div className="w-full flex-1 space-y-6">
|
<div className="w-full flex-1 space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
|
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
|
||||||
Open-source
|
Open-source
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
|
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/agent-infra/sandbox"
|
href="https://github.com/agent-infra/sandbox"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
AIO Sandbox
|
AIO Sandbox
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 text-lg text-zinc-400">
|
<div className="space-y-4 text-lg text-zinc-400">
|
||||||
<p>
|
<p>
|
||||||
We recommend using{" "}
|
We recommend using{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/agent-infra/sandbox"
|
href="https://github.com/agent-infra/sandbox"
|
||||||
className="underline"
|
className="underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
All-in-One Sandbox
|
All-in-One Sandbox
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
that combines Browser, Shell, File, MCP and VSCode Server in a
|
that combines Browser, Shell, File, MCP and VSCode Server in a
|
||||||
single Docker container.
|
single Docker container.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feature Tags */}
|
{/* Feature Tags */}
|
||||||
<div className="flex flex-wrap gap-3 pt-4">
|
<div className="flex flex-wrap gap-3 pt-4">
|
||||||
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
Isolated
|
Isolated
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
Safe
|
Safe
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
Persistent
|
Persistent
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
Mountable FS
|
Mountable FS
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
Long-running
|
Long-running
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
|
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
|
||||||
import { Section } from "../section";
|
import { Section } from "../section";
|
||||||
|
|
||||||
export function SkillsSection({ className }: { className?: string }) {
|
export function SkillsSection({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
|
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
|
||||||
title="Agent Skills"
|
title="Agent Skills"
|
||||||
subtitle={
|
subtitle={
|
||||||
<div>
|
<div>
|
||||||
Agent Skills are loaded progressively — only what's needed, when
|
Agent Skills are loaded progressively — only what's needed, when
|
||||||
it's needed.
|
it's needed.
|
||||||
<br />
|
<br />
|
||||||
Extend CM Assistant with your own skill files, or use our built-in
|
Extend CM Assistant with your own skill files, or use our built-in
|
||||||
library.
|
library.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative overflow-hidden">
|
<div className="relative overflow-hidden">
|
||||||
<ProgressiveSkillsAnimation />
|
<ProgressiveSkillsAnimation />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,63 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
|
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Section } from "../section";
|
import { Section } from "../section";
|
||||||
|
|
||||||
const COLOR = "#0a0a0a";
|
const COLOR = "#0a0a0a";
|
||||||
const features: BentoCardProps[] = [
|
const features: BentoCardProps[] = [
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Context Engineering",
|
label: "Context Engineering",
|
||||||
title: "Long/Short-term Memory",
|
title: "Long/Short-term Memory",
|
||||||
description: "Now the agent can better understand you",
|
description: "Now the agent can better understand you",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Long Task Running",
|
label: "Long Task Running",
|
||||||
title: "Planning and Sub-tasking",
|
title: "Planning and Sub-tasking",
|
||||||
description:
|
description:
|
||||||
"Plans ahead, reasons through complexity, then executes sequentially or in parallel",
|
"Plans ahead, reasons through complexity, then executes sequentially or in parallel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Extensible",
|
label: "Extensible",
|
||||||
title: "Skills and Tools",
|
title: "Skills and Tools",
|
||||||
description:
|
description:
|
||||||
"Plug, play, or even swap built-in tools. Build the agent you want.",
|
"Plug, play, or even swap built-in tools. Build the agent you want.",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Persistent",
|
label: "Persistent",
|
||||||
title: "Sandbox with File System",
|
title: "Sandbox with File System",
|
||||||
description: "Read, write, run — like a real computer",
|
description: "Read, write, run — like a real computer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Flexible",
|
label: "Flexible",
|
||||||
title: "Multi-Model Support",
|
title: "Multi-Model Support",
|
||||||
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
|
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: COLOR,
|
color: COLOR,
|
||||||
label: "Free",
|
label: "Free",
|
||||||
title: "Open Source",
|
title: "Open Source",
|
||||||
description: "MIT License, self-hosted, full control",
|
description: "MIT License, self-hosted, full control",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function WhatsNewSection({ className }: { className?: string }) {
|
export function WhatsNewSection({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
className={cn("", className)}
|
className={cn("", className)}
|
||||||
title="CM 助手"
|
title="CM 助手"
|
||||||
subtitle="CM 助手"
|
subtitle="CM 助手"
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-center">
|
<div className="flex w-full items-center justify-center">
|
||||||
<MagicBento data={features} />
|
<MagicBento data={features} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ type PageSidebarProps = {
|
|||||||
headerHidden?: boolean;
|
headerHidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "encoder" | "about";
|
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||||
|
|
||||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean =>
|
const hasActiveChild = (item: MenuItem, pathname: string): boolean =>
|
||||||
menuItemContainsPath(item, pathname);
|
menuItemContainsPath(item, pathname);
|
||||||
@@ -270,12 +270,6 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
|||||||
模型配置
|
模型配置
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
|
||||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
|
||||||
<Cpu className="size-4" />
|
|
||||||
本地编码器
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => navigate("/page/strategy/settings/model-concurrency")}
|
onClick={() => navigate("/page/strategy/settings/model-concurrency")}
|
||||||
@@ -591,12 +585,6 @@ function LegacyAccordionSidebar({
|
|||||||
模型配置
|
模型配置
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
|
||||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
|
||||||
<Cpu className="size-4" />
|
|
||||||
本地编码器
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/login-whitelist")}>
|
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/login-whitelist")}>
|
||||||
<ShieldCheck className="size-4" />
|
<ShieldCheck className="size-4" />
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
QueryClientProvider as TanStackQueryClientProvider,
|
QueryClientProvider as TanStackQueryClientProvider,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
export const queryClient = new QueryClient();
|
export const queryClient = new QueryClient();
|
||||||
|
|
||||||
export function QueryClientProvider({
|
export function QueryClientProvider({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<TanStackQueryClientProvider client={queryClient}>
|
<TanStackQueryClientProvider client={queryClient}>
|
||||||
{children}
|
{children}
|
||||||
</TanStackQueryClientProvider>
|
</TanStackQueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<NextThemesProvider
|
<NextThemesProvider
|
||||||
{...props}
|
{...props}
|
||||||
forcedTheme={pathname === "/landing" ? "dark" : undefined}
|
forcedTheme={pathname === "/landing" ? "dark" : undefined}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,66 +1,66 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-card text-card-foreground",
|
default: "bg-card text-card-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Alert({
|
function Alert({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert"
|
data-slot="alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-title"
|
data-slot="alert-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDescription({
|
function AlertDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="alert-description"
|
data-slot="alert-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription };
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
@@ -1,43 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
|
|
||||||
interface AuroraTextProps {
|
interface AuroraTextProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
colors?: string[];
|
colors?: string[];
|
||||||
speed?: number;
|
speed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuroraText = memo(
|
export const AuroraText = memo(
|
||||||
({
|
({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||||
speed = 1,
|
speed = 1,
|
||||||
}: AuroraTextProps) => {
|
}: AuroraTextProps) => {
|
||||||
const gradientStyle = {
|
const gradientStyle = {
|
||||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||||
colors[0]
|
colors[0]
|
||||||
})`,
|
})`,
|
||||||
WebkitBackgroundClip: "text",
|
WebkitBackgroundClip: "text",
|
||||||
WebkitTextFillColor: "transparent",
|
WebkitTextFillColor: "transparent",
|
||||||
animationDuration: `${10 / speed}s`,
|
animationDuration: `${10 / speed}s`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`relative inline-block ${className}`}>
|
<span className={`relative inline-block ${className}`}>
|
||||||
<span className="sr-only">{children}</span>
|
<span className="sr-only">{children}</span>
|
||||||
<span
|
<span
|
||||||
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
||||||
style={gradientStyle}
|
style={gradientStyle}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
AuroraText.displayName = "AuroraText";
|
AuroraText.displayName = "AuroraText";
|
||||||
@@ -1,53 +1,53 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Avatar({
|
function Avatar({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
data-slot="avatar"
|
data-slot="avatar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarImage({
|
function AvatarImage({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
data-slot="avatar-image"
|
data-slot="avatar-image"
|
||||||
className={cn("aspect-square size-full", className)}
|
className={cn("aspect-square size-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarFallback({
|
function AvatarFallback({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
data-slot="avatar-fallback"
|
data-slot="avatar-fallback"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback };
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span"> &
|
}: React.ComponentProps<"span"> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "span";
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="badge"
|
data-slot="badge"
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
export { Badge, badgeVariants };
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
return (
|
return (
|
||||||
<ol
|
<ol
|
||||||
data-slot="breadcrumb-list"
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-slot="breadcrumb-item"
|
data-slot="breadcrumb-item"
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
const BreadcrumbLink = React.forwardRef<
|
||||||
HTMLAnchorElement,
|
HTMLAnchorElement,
|
||||||
React.ComponentProps<"a"> & {
|
React.ComponentProps<"a"> & {
|
||||||
@@ -50,62 +50,62 @@ const BreadcrumbLink = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="breadcrumb-page"
|
data-slot="breadcrumb-page"
|
||||||
role="link"
|
role="link"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
className={cn("text-foreground font-normal", className)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
function BreadcrumbSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) {
|
}: React.ComponentProps<"li">) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
data-slot="breadcrumb-separator"
|
data-slot="breadcrumb-separator"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <ChevronRight />}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
function BreadcrumbEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="breadcrumb-ellipsis"
|
data-slot="breadcrumb-ellipsis"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="size-4" />
|
<MoreHorizontal className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis,
|
||||||
};
|
};
|
||||||
@@ -1,83 +1,83 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
horizontal:
|
horizontal:
|
||||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||||
vertical:
|
vertical:
|
||||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function ButtonGroup({
|
function ButtonGroup({
|
||||||
className,
|
className,
|
||||||
orientation,
|
orientation,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="button-group"
|
data-slot="button-group"
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupText({
|
function ButtonGroupText({
|
||||||
className,
|
className,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "div";
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupSeparator({
|
function ButtonGroupSeparator({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
return (
|
return (
|
||||||
<Separator
|
<Separator
|
||||||
data-slot="button-group-separator"
|
data-slot="button-group-separator"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ButtonGroupSeparator,
|
ButtonGroupSeparator,
|
||||||
ButtonGroupText,
|
ButtonGroupText,
|
||||||
buttonGroupVariants,
|
buttonGroupVariants,
|
||||||
};
|
};
|
||||||
@@ -1,63 +1,63 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"cursor-pointer bg-primary text-[var(--button-foreground,var(--primary-foreground))] hover:bg-primary/90",
|
"cursor-pointer bg-primary text-[var(--button-foreground,var(--primary-foreground))] hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]",
|
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]",
|
||||||
secondary:
|
secondary:
|
||||||
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost:
|
||||||
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
|
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
"icon-sm": "size-8",
|
"icon-sm": "size-8",
|
||||||
"icon-lg": "size-10",
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Button = React.forwardRef<
|
const Button = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> &
|
React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
|
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
{...(variant !== undefined && { "data-variant": variant })}
|
{...(variant !== undefined && { "data-variant": variant })}
|
||||||
{...(size !== undefined && { "data-size": size })}
|
{...(size !== undefined && { "data-size": size })}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants };
|
||||||
@@ -1,92 +1,92 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-[4px] border py-6 shadow-sm dark:border-[#0049A5] dark:bg-[#02244C]",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-[4px] border py-6 shadow-sm dark:border-[#0049A5] dark:bg-[#02244C]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
className={cn("leading-none font-semibold", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
className={cn("text-muted-foreground text-sm dark:text-[#F2F3F5]", className)}
|
className={cn("text-muted-foreground text-sm dark:text-[#F2F3F5]", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-content"
|
data-slot="card-content"
|
||||||
className={cn("px-6 ", className)}
|
className={cn("px-6 ", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
};
|
};
|
||||||
@@ -1,241 +1,241 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
} from "embla-carousel-react";
|
} from "embla-carousel-react";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1];
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
type CarouselOptions = UseCarouselParameters[0];
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
type CarouselPlugin = UseCarouselParameters[1];
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
type CarouselProps = {
|
type CarouselProps = {
|
||||||
opts?: CarouselOptions;
|
opts?: CarouselOptions;
|
||||||
plugins?: CarouselPlugin;
|
plugins?: CarouselPlugin;
|
||||||
orientation?: "horizontal" | "vertical";
|
orientation?: "horizontal" | "vertical";
|
||||||
setApi?: (api: CarouselApi) => void;
|
setApi?: (api: CarouselApi) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CarouselContextProps = {
|
type CarouselContextProps = {
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
scrollPrev: () => void;
|
scrollPrev: () => void;
|
||||||
scrollNext: () => void;
|
scrollNext: () => void;
|
||||||
canScrollPrev: boolean;
|
canScrollPrev: boolean;
|
||||||
canScrollNext: boolean;
|
canScrollNext: boolean;
|
||||||
} & CarouselProps;
|
} & CarouselProps;
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
function useCarousel() {
|
function useCarousel() {
|
||||||
const context = React.useContext(CarouselContext);
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useCarousel must be used within a <Carousel />");
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Carousel({
|
function Carousel({
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
opts,
|
opts,
|
||||||
setApi,
|
setApi,
|
||||||
plugins,
|
plugins,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
const [carouselRef, api] = useEmblaCarousel(
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
{
|
{
|
||||||
...opts,
|
...opts,
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
);
|
);
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
setCanScrollPrev(api.canScrollPrev());
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
setCanScrollNext(api.canScrollNext());
|
setCanScrollNext(api.canScrollNext());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
const scrollPrev = React.useCallback(() => {
|
||||||
api?.scrollPrev();
|
api?.scrollPrev();
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
const scrollNext = React.useCallback(() => {
|
||||||
api?.scrollNext();
|
api?.scrollNext();
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === "ArrowLeft") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
scrollPrev();
|
scrollPrev();
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === "ArrowRight") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
scrollNext();
|
scrollNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scrollPrev, scrollNext],
|
[scrollPrev, scrollNext],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api || !setApi) return;
|
if (!api || !setApi) return;
|
||||||
setApi(api);
|
setApi(api);
|
||||||
}, [api, setApi]);
|
}, [api, setApi]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
onSelect(api);
|
onSelect(api);
|
||||||
api.on("reInit", onSelect);
|
api.on("reInit", onSelect);
|
||||||
api.on("select", onSelect);
|
api.on("select", onSelect);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
api?.off("select", onSelect);
|
api?.off("select", onSelect);
|
||||||
};
|
};
|
||||||
}, [api, onSelect]);
|
}, [api, onSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContext.Provider
|
<CarouselContext.Provider
|
||||||
value={{
|
value={{
|
||||||
carouselRef,
|
carouselRef,
|
||||||
api: api,
|
api: api,
|
||||||
opts,
|
opts,
|
||||||
orientation:
|
orientation:
|
||||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
scrollPrev,
|
scrollPrev,
|
||||||
scrollNext,
|
scrollNext,
|
||||||
canScrollPrev,
|
canScrollPrev,
|
||||||
canScrollNext,
|
canScrollNext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
className={cn("relative", className)}
|
className={cn("relative", className)}
|
||||||
role="region"
|
role="region"
|
||||||
aria-roledescription="carousel"
|
aria-roledescription="carousel"
|
||||||
data-slot="carousel"
|
data-slot="carousel"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { carouselRef, orientation } = useCarousel();
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
data-slot="carousel-content"
|
data-slot="carousel-content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"flex",
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { orientation } = useCarousel();
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
data-slot="carousel-item"
|
data-slot="carousel-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselPrevious({
|
function CarouselPrevious({
|
||||||
className,
|
className,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-slot="carousel-previous"
|
data-slot="carousel-previous"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute size-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -left-12 -translate-y-1/2"
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselNext({
|
function CarouselNext({
|
||||||
className,
|
className,
|
||||||
variant = "outline",
|
variant = "outline",
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-slot="carousel-next"
|
data-slot="carousel-next"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute size-8 rounded-full",
|
"absolute size-8 rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -right-12 -translate-y-1/2"
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type CarouselApi,
|
type CarouselApi,
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
CarouselItem,
|
CarouselItem,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
CarouselNext,
|
CarouselNext,
|
||||||
};
|
};
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Collapsible({
|
function Collapsible({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollapsibleTrigger = React.forwardRef<
|
const CollapsibleTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>,
|
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger>
|
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger>
|
||||||
@@ -43,5 +43,5 @@ const CollapsibleContent = React.forwardRef<
|
|||||||
|
|
||||||
CollapsibleContent.displayName =
|
CollapsibleContent.displayName =
|
||||||
CollapsiblePrimitive.CollapsibleContent.displayName;
|
CollapsiblePrimitive.CollapsibleContent.displayName;
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
@@ -1,181 +1,181 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Command as CommandPrimitive } from "cmdk";
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { SearchIcon } from "lucide-react";
|
import { SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
function Command({
|
function Command({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
data-slot="command"
|
data-slot="command"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandDialog({
|
function CommandDialog({
|
||||||
title = "Command Palette",
|
title = "Command Palette",
|
||||||
description = "Search for a command to run...",
|
description = "Search for a command to run...",
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Dialog> & {
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn("overflow-hidden p-0", className)}
|
className={cn("overflow-hidden p-0", className)}
|
||||||
showCloseButton={showCloseButton}
|
showCloseButton={showCloseButton}
|
||||||
>
|
>
|
||||||
<DialogTitle className="sr-only">{title}</DialogTitle>
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
||||||
<DialogDescription className="sr-only">{description}</DialogDescription>
|
<DialogDescription className="sr-only">{description}</DialogDescription>
|
||||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandInput({
|
function CommandInput({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="command-input-wrapper"
|
data-slot="command-input-wrapper"
|
||||||
className="flex h-9 items-center gap-2 border-b px-3"
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
>
|
>
|
||||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandList({
|
function CommandList({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.List
|
<CommandPrimitive.List
|
||||||
data-slot="command-list"
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandEmpty({
|
function CommandEmpty({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Empty
|
<CommandPrimitive.Empty
|
||||||
data-slot="command-empty"
|
data-slot="command-empty"
|
||||||
className="py-6 text-center text-sm"
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandGroup({
|
function CommandGroup({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandSeparator({
|
function CommandSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Separator
|
<CommandPrimitive.Separator
|
||||||
data-slot="command-separator"
|
data-slot="command-separator"
|
||||||
className={cn("bg-border -mx-1 h-px", className)}
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItem({
|
function CommandItem({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandShortcut({
|
function CommandShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="command-shortcut"
|
data-slot="command-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
};
|
};
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { type MouseEventHandler } from "react";
|
import React, { type MouseEventHandler } from "react";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
|
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
|
||||||
angle?: number;
|
angle?: number;
|
||||||
particleCount?: number;
|
particleCount?: number;
|
||||||
startVelocity?: number;
|
startVelocity?: number;
|
||||||
spread?: number;
|
spread?: number;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfettiButton({
|
export function ConfettiButton({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
angle = 90,
|
angle = 90,
|
||||||
particleCount = 75,
|
particleCount = 75,
|
||||||
startVelocity = 35,
|
startVelocity = 35,
|
||||||
spread = 70,
|
spread = 70,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}: ConfettiButtonProps) {
|
}: ConfettiButtonProps) {
|
||||||
const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||||
const target = event.currentTarget;
|
const target = event.currentTarget;
|
||||||
if (target) {
|
if (target) {
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
confetti({
|
confetti({
|
||||||
particleCount,
|
particleCount,
|
||||||
startVelocity,
|
startVelocity,
|
||||||
angle,
|
angle,
|
||||||
spread,
|
spread,
|
||||||
origin: {
|
origin: {
|
||||||
x: (rect.left + rect.width / 2) / window.innerWidth,
|
x: (rect.left + rect.width / 2) / window.innerWidth,
|
||||||
y: (rect.top + rect.height / 2) / window.innerHeight,
|
y: (rect.top + rect.height / 2) / window.innerHeight,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleClick} className={className} {...props}>
|
<Button onClick={handleClick} className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,76 +1,76 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { isWujieEnv } from "@/core/embed/wujie";
|
import { isWujieEnv } from "@/core/embed/wujie";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
modal,
|
modal,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
// 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。
|
// 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。
|
||||||
// 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。
|
// 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Root
|
<DialogPrimitive.Root
|
||||||
data-slot="dialog"
|
data-slot="dialog"
|
||||||
modal={modal ?? !isWujieEnv()}
|
modal={modal ?? !isWujieEnv()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogTrigger = React.forwardRef<
|
const DialogTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Trigger>,
|
React.ElementRef<typeof DialogPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Trigger
|
<DialogPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="dialog-trigger"
|
data-slot="dialog-trigger"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
|
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogClose = React.forwardRef<
|
const DialogClose = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Close>,
|
React.ElementRef<typeof DialogPrimitive.Close>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />;
|
return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
DialogClose.displayName = DialogPrimitive.Close.displayName;
|
DialogClose.displayName = DialogPrimitive.Close.displayName;
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
@@ -82,90 +82,90 @@ const DialogContent = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay className={overlayClassName} />
|
<DialogOverlay className={overlayClassName} />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
};
|
};
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
function DropdownMenuPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuTrigger = React.forwardRef<
|
const DropdownMenuTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
||||||
@@ -34,7 +34,7 @@ const DropdownMenuTrigger = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
|
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
@@ -56,15 +56,15 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
function DropdownMenuGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
@@ -88,123 +88,123 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
checked,
|
checked,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
function DropdownMenuRadioItem({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CircleIcon className="size-2 fill-current" />
|
<CircleIcon className="size-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
function DropdownMenuLabel({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
function DropdownMenuSeparator({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSub({
|
function DropdownMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
@@ -230,7 +230,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName =
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
@@ -250,21 +250,21 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
|
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName =
|
||||||
DropdownMenuPrimitive.SubContent.displayName;
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
};
|
};
|
||||||
@@ -1,104 +1,104 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty"
|
data-slot="empty"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-header"
|
data-slot="empty-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
const emptyMediaVariants = cva(
|
||||||
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6 dark:text-[#55A6FE] dark:bg-[#022248]",
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6 dark:text-[#55A6FE] dark:bg-[#022248]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function EmptyMedia({
|
function EmptyMedia({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-icon"
|
data-slot="empty-icon"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-title"
|
data-slot="empty-title"
|
||||||
className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)}
|
className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-description"
|
data-slot="empty-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]",
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-content"
|
data-slot="empty-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
};
|
};
|
||||||
@@ -1,202 +1,202 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
squareSize?: number;
|
squareSize?: number;
|
||||||
gridGap?: number;
|
gridGap?: number;
|
||||||
flickerChance?: number;
|
flickerChance?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
maxOpacity?: number;
|
maxOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||||
squareSize = 4,
|
squareSize = 4,
|
||||||
gridGap = 6,
|
gridGap = 6,
|
||||||
flickerChance = 0.3,
|
flickerChance = 0.3,
|
||||||
color = "rgb(0, 0, 0)",
|
color = "rgb(0, 0, 0)",
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
className,
|
className,
|
||||||
maxOpacity = 0.3,
|
maxOpacity = 0.3,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isInView, setIsInView] = useState(false);
|
const [isInView, setIsInView] = useState(false);
|
||||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
const memoizedColor = useMemo(() => {
|
const memoizedColor = useMemo(() => {
|
||||||
const toRGBA = (color: string) => {
|
const toRGBA = (color: string) => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return `rgba(0, 0, 0,`;
|
return `rgba(0, 0, 0,`;
|
||||||
}
|
}
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = canvas.height = 1;
|
canvas.width = canvas.height = 1;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return "rgba(255, 0, 0,";
|
if (!ctx) return "rgba(255, 0, 0,";
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(0, 0, 1, 1);
|
ctx.fillRect(0, 0, 1, 1);
|
||||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||||
return `rgba(${r}, ${g}, ${b},`;
|
return `rgba(${r}, ${g}, ${b},`;
|
||||||
};
|
};
|
||||||
return toRGBA(color);
|
return toRGBA(color);
|
||||||
}, [color]);
|
}, [color]);
|
||||||
|
|
||||||
const setupCanvas = useCallback(
|
const setupCanvas = useCallback(
|
||||||
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
canvas.width = width * dpr;
|
canvas.width = width * dpr;
|
||||||
canvas.height = height * dpr;
|
canvas.height = height * dpr;
|
||||||
canvas.style.width = `${width}px`;
|
canvas.style.width = `${width}px`;
|
||||||
canvas.style.height = `${height}px`;
|
canvas.style.height = `${height}px`;
|
||||||
const cols = Math.floor(width / (squareSize + gridGap));
|
const cols = Math.floor(width / (squareSize + gridGap));
|
||||||
const rows = Math.floor(height / (squareSize + gridGap));
|
const rows = Math.floor(height / (squareSize + gridGap));
|
||||||
|
|
||||||
const squares = new Float32Array(cols * rows);
|
const squares = new Float32Array(cols * rows);
|
||||||
for (let i = 0; i < squares.length; i++) {
|
for (let i = 0; i < squares.length; i++) {
|
||||||
squares[i] = Math.random() * maxOpacity;
|
squares[i] = Math.random() * maxOpacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cols, rows, squares, dpr };
|
return { cols, rows, squares, dpr };
|
||||||
},
|
},
|
||||||
[squareSize, gridGap, maxOpacity],
|
[squareSize, gridGap, maxOpacity],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSquares = useCallback(
|
const updateSquares = useCallback(
|
||||||
(squares: Float32Array, deltaTime: number) => {
|
(squares: Float32Array, deltaTime: number) => {
|
||||||
for (let i = 0; i < squares.length; i++) {
|
for (let i = 0; i < squares.length; i++) {
|
||||||
if (Math.random() < flickerChance * deltaTime) {
|
if (Math.random() < flickerChance * deltaTime) {
|
||||||
squares[i] = Math.random() * maxOpacity;
|
squares[i] = Math.random() * maxOpacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[flickerChance, maxOpacity],
|
[flickerChance, maxOpacity],
|
||||||
);
|
);
|
||||||
|
|
||||||
const drawGrid = useCallback(
|
const drawGrid = useCallback(
|
||||||
(
|
(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
cols: number,
|
cols: number,
|
||||||
rows: number,
|
rows: number,
|
||||||
squares: Float32Array,
|
squares: Float32Array,
|
||||||
dpr: number,
|
dpr: number,
|
||||||
) => {
|
) => {
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
ctx.fillStyle = "transparent";
|
ctx.fillStyle = "transparent";
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
for (let j = 0; j < rows; j++) {
|
for (let j = 0; j < rows; j++) {
|
||||||
const opacity = squares[i * rows + j];
|
const opacity = squares[i * rows + j];
|
||||||
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
||||||
ctx.fillRect(
|
ctx.fillRect(
|
||||||
i * (squareSize + gridGap) * dpr,
|
i * (squareSize + gridGap) * dpr,
|
||||||
j * (squareSize + gridGap) * dpr,
|
j * (squareSize + gridGap) * dpr,
|
||||||
squareSize * dpr,
|
squareSize * dpr,
|
||||||
squareSize * dpr,
|
squareSize * dpr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[memoizedColor, squareSize, gridGap],
|
[memoizedColor, squareSize, gridGap],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!canvas || !container) return;
|
if (!canvas || !container) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
let animationFrameId: number;
|
let animationFrameId: number;
|
||||||
let gridParams: ReturnType<typeof setupCanvas>;
|
let gridParams: ReturnType<typeof setupCanvas>;
|
||||||
|
|
||||||
const updateCanvasSize = () => {
|
const updateCanvasSize = () => {
|
||||||
const newWidth = width || container.clientWidth;
|
const newWidth = width || container.clientWidth;
|
||||||
const newHeight = height || container.clientHeight;
|
const newHeight = height || container.clientHeight;
|
||||||
setCanvasSize({ width: newWidth, height: newHeight });
|
setCanvasSize({ width: newWidth, height: newHeight });
|
||||||
gridParams = setupCanvas(canvas, newWidth, newHeight);
|
gridParams = setupCanvas(canvas, newWidth, newHeight);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
|
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
const animate = (time: number) => {
|
const animate = (time: number) => {
|
||||||
if (!isInView) return;
|
if (!isInView) return;
|
||||||
|
|
||||||
const deltaTime = (time - lastTime) / 1000;
|
const deltaTime = (time - lastTime) / 1000;
|
||||||
lastTime = time;
|
lastTime = time;
|
||||||
|
|
||||||
updateSquares(gridParams.squares, deltaTime);
|
updateSquares(gridParams.squares, deltaTime);
|
||||||
drawGrid(
|
drawGrid(
|
||||||
ctx,
|
ctx,
|
||||||
canvas.width,
|
canvas.width,
|
||||||
canvas.height,
|
canvas.height,
|
||||||
gridParams.cols,
|
gridParams.cols,
|
||||||
gridParams.rows,
|
gridParams.rows,
|
||||||
gridParams.squares,
|
gridParams.squares,
|
||||||
gridParams.dpr,
|
gridParams.dpr,
|
||||||
);
|
);
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
const intersectionObserver = new IntersectionObserver(
|
const intersectionObserver = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (entry) {
|
if (entry) {
|
||||||
setIsInView(entry.isIntersecting);
|
setIsInView(entry.isIntersecting);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0 },
|
{ threshold: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
intersectionObserver.observe(canvas);
|
intersectionObserver.observe(canvas);
|
||||||
|
|
||||||
if (isInView) {
|
if (isInView) {
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animationFrameId);
|
cancelAnimationFrame(animationFrameId);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
intersectionObserver.disconnect();
|
intersectionObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn(`h-full w-full ${className}`)}
|
className={cn(`h-full w-full ${className}`)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="pointer-events-none"
|
className="pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
width: canvasSize.width,
|
width: canvasSize.width,
|
||||||
height: canvasSize.height,
|
height: canvasSize.height,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.galaxy-container {
|
.galaxy-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -1,361 +1,361 @@
|
|||||||
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
|
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import "./galaxy.css";
|
import "./galaxy.css";
|
||||||
|
|
||||||
const vertexShader = `
|
const vertexShader = `
|
||||||
attribute vec2 uv;
|
attribute vec2 uv;
|
||||||
attribute vec2 position;
|
attribute vec2 position;
|
||||||
|
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vUv = uv;
|
vUv = uv;
|
||||||
gl_Position = vec4(position, 0, 1);
|
gl_Position = vec4(position, 0, 1);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fragmentShader = `
|
const fragmentShader = `
|
||||||
precision highp float;
|
precision highp float;
|
||||||
|
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
uniform vec3 uResolution;
|
uniform vec3 uResolution;
|
||||||
uniform vec2 uFocal;
|
uniform vec2 uFocal;
|
||||||
uniform vec2 uRotation;
|
uniform vec2 uRotation;
|
||||||
uniform float uStarSpeed;
|
uniform float uStarSpeed;
|
||||||
uniform float uDensity;
|
uniform float uDensity;
|
||||||
uniform float uHueShift;
|
uniform float uHueShift;
|
||||||
uniform float uSpeed;
|
uniform float uSpeed;
|
||||||
uniform vec2 uMouse;
|
uniform vec2 uMouse;
|
||||||
uniform float uGlowIntensity;
|
uniform float uGlowIntensity;
|
||||||
uniform float uSaturation;
|
uniform float uSaturation;
|
||||||
uniform bool uMouseRepulsion;
|
uniform bool uMouseRepulsion;
|
||||||
uniform float uTwinkleIntensity;
|
uniform float uTwinkleIntensity;
|
||||||
uniform float uRotationSpeed;
|
uniform float uRotationSpeed;
|
||||||
uniform float uRepulsionStrength;
|
uniform float uRepulsionStrength;
|
||||||
uniform float uMouseActiveFactor;
|
uniform float uMouseActiveFactor;
|
||||||
uniform float uAutoCenterRepulsion;
|
uniform float uAutoCenterRepulsion;
|
||||||
uniform bool uTransparent;
|
uniform bool uTransparent;
|
||||||
|
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
#define NUM_LAYER 4.0
|
#define NUM_LAYER 4.0
|
||||||
#define STAR_COLOR_CUTOFF 0.2
|
#define STAR_COLOR_CUTOFF 0.2
|
||||||
#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)
|
#define MAT45 mat2(0.7071, -0.7071, 0.7071, 0.7071)
|
||||||
#define PERIOD 3.0
|
#define PERIOD 3.0
|
||||||
|
|
||||||
float Hash21(vec2 p) {
|
float Hash21(vec2 p) {
|
||||||
p = fract(p * vec2(123.34, 456.21));
|
p = fract(p * vec2(123.34, 456.21));
|
||||||
p += dot(p, p + 45.32);
|
p += dot(p, p + 45.32);
|
||||||
return fract(p.x * p.y);
|
return fract(p.x * p.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
float tri(float x) {
|
float tri(float x) {
|
||||||
return abs(fract(x) * 2.0 - 1.0);
|
return abs(fract(x) * 2.0 - 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
float tris(float x) {
|
float tris(float x) {
|
||||||
float t = fract(x);
|
float t = fract(x);
|
||||||
return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));
|
return 1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
float trisn(float x) {
|
float trisn(float x) {
|
||||||
float t = fract(x);
|
float t = fract(x);
|
||||||
return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;
|
return 2.0 * (1.0 - smoothstep(0.0, 1.0, abs(2.0 * t - 1.0))) - 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 hsv2rgb(vec3 c) {
|
vec3 hsv2rgb(vec3 c) {
|
||||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
float Star(vec2 uv, float flare) {
|
float Star(vec2 uv, float flare) {
|
||||||
float d = length(uv);
|
float d = length(uv);
|
||||||
float m = (0.05 * uGlowIntensity) / d;
|
float m = (0.05 * uGlowIntensity) / d;
|
||||||
float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
|
float rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
|
||||||
m += rays * flare * uGlowIntensity;
|
m += rays * flare * uGlowIntensity;
|
||||||
uv *= MAT45;
|
uv *= MAT45;
|
||||||
rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
|
rays = smoothstep(0.0, 1.0, 1.0 - abs(uv.x * uv.y * 1000.0));
|
||||||
m += rays * 0.3 * flare * uGlowIntensity;
|
m += rays * 0.3 * flare * uGlowIntensity;
|
||||||
m *= smoothstep(1.0, 0.2, d);
|
m *= smoothstep(1.0, 0.2, d);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 StarLayer(vec2 uv) {
|
vec3 StarLayer(vec2 uv) {
|
||||||
vec3 col = vec3(0.0);
|
vec3 col = vec3(0.0);
|
||||||
|
|
||||||
vec2 gv = fract(uv) - 0.5;
|
vec2 gv = fract(uv) - 0.5;
|
||||||
vec2 id = floor(uv);
|
vec2 id = floor(uv);
|
||||||
|
|
||||||
for (int y = -1; y <= 1; y++) {
|
for (int y = -1; y <= 1; y++) {
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
vec2 offset = vec2(float(x), float(y));
|
vec2 offset = vec2(float(x), float(y));
|
||||||
vec2 si = id + vec2(float(x), float(y));
|
vec2 si = id + vec2(float(x), float(y));
|
||||||
float seed = Hash21(si);
|
float seed = Hash21(si);
|
||||||
float size = fract(seed * 345.32);
|
float size = fract(seed * 345.32);
|
||||||
float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));
|
float glossLocal = tri(uStarSpeed / (PERIOD * seed + 1.0));
|
||||||
float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;
|
float flareSize = smoothstep(0.9, 1.0, size) * glossLocal;
|
||||||
|
|
||||||
float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;
|
float red = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 1.0)) + STAR_COLOR_CUTOFF;
|
||||||
float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
|
float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
|
||||||
float grn = min(red, blu) * seed;
|
float grn = min(red, blu) * seed;
|
||||||
vec3 base = vec3(red, grn, blu);
|
vec3 base = vec3(red, grn, blu);
|
||||||
|
|
||||||
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
|
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
|
||||||
hue = fract(hue + uHueShift / 360.0);
|
hue = fract(hue + uHueShift / 360.0);
|
||||||
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;
|
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;
|
||||||
float val = max(max(base.r, base.g), base.b);
|
float val = max(max(base.r, base.g), base.b);
|
||||||
base = hsv2rgb(vec3(hue, sat, val));
|
base = hsv2rgb(vec3(hue, sat, val));
|
||||||
|
|
||||||
vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;
|
vec2 pad = vec2(tris(seed * 34.0 + uTime * uSpeed / 10.0), tris(seed * 38.0 + uTime * uSpeed / 30.0)) - 0.5;
|
||||||
|
|
||||||
float star = Star(gv - offset - pad, flareSize);
|
float star = Star(gv - offset - pad, flareSize);
|
||||||
vec3 color = base;
|
vec3 color = base;
|
||||||
|
|
||||||
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
|
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
|
||||||
twinkle = mix(1.0, twinkle, uTwinkleIntensity);
|
twinkle = mix(1.0, twinkle, uTwinkleIntensity);
|
||||||
star *= twinkle;
|
star *= twinkle;
|
||||||
|
|
||||||
col += star * size * color;
|
col += star * size * color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 focalPx = uFocal * uResolution.xy;
|
vec2 focalPx = uFocal * uResolution.xy;
|
||||||
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
|
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
|
||||||
|
|
||||||
vec2 mouseNorm = uMouse - vec2(0.5);
|
vec2 mouseNorm = uMouse - vec2(0.5);
|
||||||
|
|
||||||
if (uAutoCenterRepulsion > 0.0) {
|
if (uAutoCenterRepulsion > 0.0) {
|
||||||
vec2 centerUV = vec2(0.0, 0.0);
|
vec2 centerUV = vec2(0.0, 0.0);
|
||||||
float centerDist = length(uv - centerUV);
|
float centerDist = length(uv - centerUV);
|
||||||
vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));
|
vec2 repulsion = normalize(uv - centerUV) * (uAutoCenterRepulsion / (centerDist + 0.1));
|
||||||
uv += repulsion * 0.05;
|
uv += repulsion * 0.05;
|
||||||
} else if (uMouseRepulsion) {
|
} else if (uMouseRepulsion) {
|
||||||
vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;
|
vec2 mousePosUV = (uMouse * uResolution.xy - focalPx) / uResolution.y;
|
||||||
float mouseDist = length(uv - mousePosUV);
|
float mouseDist = length(uv - mousePosUV);
|
||||||
vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));
|
vec2 repulsion = normalize(uv - mousePosUV) * (uRepulsionStrength / (mouseDist + 0.1));
|
||||||
uv += repulsion * 0.05 * uMouseActiveFactor;
|
uv += repulsion * 0.05 * uMouseActiveFactor;
|
||||||
} else {
|
} else {
|
||||||
vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;
|
vec2 mouseOffset = mouseNorm * 0.1 * uMouseActiveFactor;
|
||||||
uv += mouseOffset;
|
uv += mouseOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
float autoRotAngle = uTime * uRotationSpeed;
|
float autoRotAngle = uTime * uRotationSpeed;
|
||||||
mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));
|
mat2 autoRot = mat2(cos(autoRotAngle), -sin(autoRotAngle), sin(autoRotAngle), cos(autoRotAngle));
|
||||||
uv = autoRot * uv;
|
uv = autoRot * uv;
|
||||||
|
|
||||||
uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;
|
uv = mat2(uRotation.x, -uRotation.y, uRotation.y, uRotation.x) * uv;
|
||||||
|
|
||||||
vec3 col = vec3(0.0);
|
vec3 col = vec3(0.0);
|
||||||
|
|
||||||
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {
|
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_LAYER) {
|
||||||
float depth = fract(i + uStarSpeed * uSpeed);
|
float depth = fract(i + uStarSpeed * uSpeed);
|
||||||
float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);
|
float scale = mix(20.0 * uDensity, 0.5 * uDensity, depth);
|
||||||
float fade = depth * smoothstep(1.0, 0.9, depth);
|
float fade = depth * smoothstep(1.0, 0.9, depth);
|
||||||
col += StarLayer(uv * scale + i * 453.32) * fade;
|
col += StarLayer(uv * scale + i * 453.32) * fade;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uTransparent) {
|
if (uTransparent) {
|
||||||
float alpha = length(col);
|
float alpha = length(col);
|
||||||
alpha = smoothstep(0.0, 0.3, alpha);
|
alpha = smoothstep(0.0, 0.3, alpha);
|
||||||
alpha = min(alpha, 1.0);
|
alpha = min(alpha, 1.0);
|
||||||
gl_FragColor = vec4(col, alpha);
|
gl_FragColor = vec4(col, alpha);
|
||||||
} else {
|
} else {
|
||||||
gl_FragColor = vec4(col, 1.0);
|
gl_FragColor = vec4(col, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Galaxy({
|
export default function Galaxy({
|
||||||
focal = [0.5, 0.5],
|
focal = [0.5, 0.5],
|
||||||
rotation = [1.0, 0.0],
|
rotation = [1.0, 0.0],
|
||||||
starSpeed = 0.5,
|
starSpeed = 0.5,
|
||||||
density = 1,
|
density = 1,
|
||||||
hueShift = 140,
|
hueShift = 140,
|
||||||
disableAnimation = false,
|
disableAnimation = false,
|
||||||
speed = 1.0,
|
speed = 1.0,
|
||||||
mouseInteraction = true,
|
mouseInteraction = true,
|
||||||
glowIntensity = 0.3,
|
glowIntensity = 0.3,
|
||||||
saturation = 0.0,
|
saturation = 0.0,
|
||||||
mouseRepulsion = true,
|
mouseRepulsion = true,
|
||||||
repulsionStrength = 2,
|
repulsionStrength = 2,
|
||||||
twinkleIntensity = 0.3,
|
twinkleIntensity = 0.3,
|
||||||
rotationSpeed = 0.1,
|
rotationSpeed = 0.1,
|
||||||
autoCenterRepulsion = 0,
|
autoCenterRepulsion = 0,
|
||||||
transparent = true,
|
transparent = true,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
const ctnDom = useRef(null);
|
const ctnDom = useRef(null);
|
||||||
const targetMousePos = useRef({ x: 0.5, y: 0.5 });
|
const targetMousePos = useRef({ x: 0.5, y: 0.5 });
|
||||||
const smoothMousePos = useRef({ x: 0.5, y: 0.5 });
|
const smoothMousePos = useRef({ x: 0.5, y: 0.5 });
|
||||||
const targetMouseActive = useRef(0.0);
|
const targetMouseActive = useRef(0.0);
|
||||||
const smoothMouseActive = useRef(0.0);
|
const smoothMouseActive = useRef(0.0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctnDom.current) return;
|
if (!ctnDom.current) return;
|
||||||
const ctn = ctnDom.current;
|
const ctn = ctnDom.current;
|
||||||
|
|
||||||
let renderer;
|
let renderer;
|
||||||
try {
|
try {
|
||||||
renderer = new Renderer({
|
renderer = new Renderer({
|
||||||
alpha: transparent,
|
alpha: transparent,
|
||||||
premultipliedAlpha: false,
|
premultipliedAlpha: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Galaxy: WebGL is not available. The galaxy background will not be rendered.",
|
"Galaxy: WebGL is not available. The galaxy background will not be rendered.",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gl = renderer.gl;
|
const gl = renderer.gl;
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Galaxy: WebGL context is null. The galaxy background will not be rendered.",
|
"Galaxy: WebGL context is null. The galaxy background will not be rendered.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transparent) {
|
if (transparent) {
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
gl.clearColor(0, 0, 0, 1);
|
gl.clearColor(0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Program | undefined} */
|
/** @type {Program | undefined} */
|
||||||
let program;
|
let program;
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
const scale = 1;
|
const scale = 1;
|
||||||
renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);
|
renderer.setSize(ctn.offsetWidth * scale, ctn.offsetHeight * scale);
|
||||||
if (program) {
|
if (program) {
|
||||||
program.uniforms.uResolution.value = new Color(
|
program.uniforms.uResolution.value = new Color(
|
||||||
gl.canvas.width,
|
gl.canvas.width,
|
||||||
gl.canvas.height,
|
gl.canvas.height,
|
||||||
gl.canvas.width / gl.canvas.height,
|
gl.canvas.width / gl.canvas.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener("resize", resize, false);
|
window.addEventListener("resize", resize, false);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
const geometry = new Triangle(gl);
|
||||||
program = new Program(gl, {
|
program = new Program(gl, {
|
||||||
vertex: vertexShader,
|
vertex: vertexShader,
|
||||||
fragment: fragmentShader,
|
fragment: fragmentShader,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
uTime: { value: 0 },
|
uTime: { value: 0 },
|
||||||
uResolution: {
|
uResolution: {
|
||||||
value: new Color(
|
value: new Color(
|
||||||
gl.canvas.width,
|
gl.canvas.width,
|
||||||
gl.canvas.height,
|
gl.canvas.height,
|
||||||
gl.canvas.width / gl.canvas.height,
|
gl.canvas.width / gl.canvas.height,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uFocal: { value: new Float32Array(focal) },
|
uFocal: { value: new Float32Array(focal) },
|
||||||
uRotation: { value: new Float32Array(rotation) },
|
uRotation: { value: new Float32Array(rotation) },
|
||||||
uStarSpeed: { value: starSpeed },
|
uStarSpeed: { value: starSpeed },
|
||||||
uDensity: { value: density },
|
uDensity: { value: density },
|
||||||
uHueShift: { value: hueShift },
|
uHueShift: { value: hueShift },
|
||||||
uSpeed: { value: speed },
|
uSpeed: { value: speed },
|
||||||
uMouse: {
|
uMouse: {
|
||||||
value: new Float32Array([
|
value: new Float32Array([
|
||||||
smoothMousePos.current.x,
|
smoothMousePos.current.x,
|
||||||
smoothMousePos.current.y,
|
smoothMousePos.current.y,
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
uGlowIntensity: { value: glowIntensity },
|
uGlowIntensity: { value: glowIntensity },
|
||||||
uSaturation: { value: saturation },
|
uSaturation: { value: saturation },
|
||||||
uMouseRepulsion: { value: mouseRepulsion },
|
uMouseRepulsion: { value: mouseRepulsion },
|
||||||
uTwinkleIntensity: { value: twinkleIntensity },
|
uTwinkleIntensity: { value: twinkleIntensity },
|
||||||
uRotationSpeed: { value: rotationSpeed },
|
uRotationSpeed: { value: rotationSpeed },
|
||||||
uRepulsionStrength: { value: repulsionStrength },
|
uRepulsionStrength: { value: repulsionStrength },
|
||||||
uMouseActiveFactor: { value: 0.0 },
|
uMouseActiveFactor: { value: 0.0 },
|
||||||
uAutoCenterRepulsion: { value: autoCenterRepulsion },
|
uAutoCenterRepulsion: { value: autoCenterRepulsion },
|
||||||
uTransparent: { value: transparent },
|
uTransparent: { value: transparent },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
let animateId;
|
let animateId;
|
||||||
|
|
||||||
function update(t) {
|
function update(t) {
|
||||||
animateId = requestAnimationFrame(update);
|
animateId = requestAnimationFrame(update);
|
||||||
if (!disableAnimation) {
|
if (!disableAnimation) {
|
||||||
program.uniforms.uTime.value = t * 0.001;
|
program.uniforms.uTime.value = t * 0.001;
|
||||||
program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;
|
program.uniforms.uStarSpeed.value = (t * 0.001 * starSpeed) / 10.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lerpFactor = 0.05;
|
const lerpFactor = 0.05;
|
||||||
smoothMousePos.current.x +=
|
smoothMousePos.current.x +=
|
||||||
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
|
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
|
||||||
smoothMousePos.current.y +=
|
smoothMousePos.current.y +=
|
||||||
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
|
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
|
||||||
|
|
||||||
smoothMouseActive.current +=
|
smoothMouseActive.current +=
|
||||||
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
|
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
|
||||||
|
|
||||||
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
|
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
|
||||||
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
|
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
|
||||||
program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;
|
program.uniforms.uMouseActiveFactor.value = smoothMouseActive.current;
|
||||||
|
|
||||||
renderer.render({ scene: mesh });
|
renderer.render({ scene: mesh });
|
||||||
}
|
}
|
||||||
animateId = requestAnimationFrame(update);
|
animateId = requestAnimationFrame(update);
|
||||||
ctn.appendChild(gl.canvas);
|
ctn.appendChild(gl.canvas);
|
||||||
|
|
||||||
function handleMouseMove(e) {
|
function handleMouseMove(e) {
|
||||||
const rect = ctn.getBoundingClientRect();
|
const rect = ctn.getBoundingClientRect();
|
||||||
const x = (e.clientX - rect.left) / rect.width;
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
const y = 1.0 - (e.clientY - rect.top) / rect.height;
|
const y = 1.0 - (e.clientY - rect.top) / rect.height;
|
||||||
targetMousePos.current = { x, y };
|
targetMousePos.current = { x, y };
|
||||||
targetMouseActive.current = 1.0;
|
targetMouseActive.current = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseLeave() {
|
function handleMouseLeave() {
|
||||||
targetMouseActive.current = 0.0;
|
targetMouseActive.current = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mouseInteraction) {
|
if (mouseInteraction) {
|
||||||
ctn.addEventListener("mousemove", handleMouseMove);
|
ctn.addEventListener("mousemove", handleMouseMove);
|
||||||
ctn.addEventListener("mouseleave", handleMouseLeave);
|
ctn.addEventListener("mouseleave", handleMouseLeave);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animateId);
|
cancelAnimationFrame(animateId);
|
||||||
window.removeEventListener("resize", resize);
|
window.removeEventListener("resize", resize);
|
||||||
if (mouseInteraction) {
|
if (mouseInteraction) {
|
||||||
ctn.removeEventListener("mousemove", handleMouseMove);
|
ctn.removeEventListener("mousemove", handleMouseMove);
|
||||||
ctn.removeEventListener("mouseleave", handleMouseLeave);
|
ctn.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
}
|
}
|
||||||
ctn.removeChild(gl.canvas);
|
ctn.removeChild(gl.canvas);
|
||||||
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
focal,
|
focal,
|
||||||
rotation,
|
rotation,
|
||||||
starSpeed,
|
starSpeed,
|
||||||
density,
|
density,
|
||||||
hueShift,
|
hueShift,
|
||||||
disableAnimation,
|
disableAnimation,
|
||||||
speed,
|
speed,
|
||||||
mouseInteraction,
|
mouseInteraction,
|
||||||
glowIntensity,
|
glowIntensity,
|
||||||
saturation,
|
saturation,
|
||||||
mouseRepulsion,
|
mouseRepulsion,
|
||||||
twinkleIntensity,
|
twinkleIntensity,
|
||||||
rotationSpeed,
|
rotationSpeed,
|
||||||
repulsionStrength,
|
repulsionStrength,
|
||||||
autoCenterRepulsion,
|
autoCenterRepulsion,
|
||||||
transparent,
|
transparent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
|
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function HoverCard({
|
function HoverCard({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HoverCardTrigger = React.forwardRef<
|
const HoverCardTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Trigger>,
|
React.ElementRef<typeof HoverCardPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Trigger>
|
||||||
@@ -48,5 +48,5 @@ const HoverCardContent = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
@@ -1,173 +1,173 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]",
|
"group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]",
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||||
|
|
||||||
// Focus state.
|
// Focus state.
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-input has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
"has-[[data-slot=input-group-control]:focus-visible]:border-input has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
"inline-start":
|
"inline-start":
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||||
"inline-end":
|
"inline-end":
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||||
"block-start":
|
"block-start":
|
||||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||||
"block-end":
|
"block-end":
|
||||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
align: "inline-start",
|
align: "inline-start",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function InputGroupAddon({
|
function InputGroupAddon({
|
||||||
className,
|
className,
|
||||||
align = "inline-start",
|
align = "inline-start",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="input-group-addon"
|
data-slot="input-group-addon"
|
||||||
data-align={align}
|
data-align={align}
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
const inputGroupButtonVariants = cva(
|
||||||
"text-sm shadow-none flex gap-2 items-center",
|
"text-sm shadow-none flex gap-2 items-center",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
||||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
||||||
"icon-xs":
|
"icon-xs":
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: "xs",
|
size: "xs",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function InputGroupButton({
|
function InputGroupButton({
|
||||||
className,
|
className,
|
||||||
type = "button",
|
type = "button",
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "xs",
|
size = "xs",
|
||||||
...props
|
...props
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type={type}
|
type={type}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InputGroupInput({
|
function InputGroupInput({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"input">) {
|
}: React.ComponentProps<"input">) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputGroupTextarea = React.forwardRef<
|
const InputGroupTextarea = React.forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
React.ComponentProps<"textarea">
|
React.ComponentProps<"textarea">
|
||||||
>(function InputGroupTextarea({ className, ...props }, ref) {
|
>(function InputGroupTextarea({ className, ...props }, ref) {
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
InputGroupTextarea.displayName = "InputGroupTextarea";
|
InputGroupTextarea.displayName = "InputGroupTextarea";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
};
|
};
|
||||||