kiku-agent 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +621 -0
- package/README.md +296 -14
- package/package.json +95 -92
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +79 -43
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +122 -8
- package/server/agent/commands.js +403 -28
- package/server/agent/jobManager.js +96 -0
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- package/server/agent/quizData.js +384 -0
- package/server/agent/terminalImage.js +248 -0
- package/server/agent/tools.js +153 -43
- package/server/agent/ui.js +67 -21
- package/server/agent/updateManager.js +21 -3
- package/src/assets/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/src/assets/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
export const QUIZ_CORRECT_ANSWERS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
2
|
+
|
|
3
|
+
export const QUIZ_I18N = {
|
|
4
|
+
th: {
|
|
5
|
+
title: "鞠婧祎蜜橘专属问答挑战 ",
|
|
6
|
+
subtitle: "ตอบคำถามทั้ง 15 ข้อให้ถูกทั้งหมดเพื่อรับ Developer VIP Key (1,000 ครั้ง/นาที)",
|
|
7
|
+
questionOf: (idx, total) => `ข้อ ${idx} / ${total}`,
|
|
8
|
+
score: (score, total) => `คะแนน: ${score} / ${total}`,
|
|
9
|
+
correct: " ถูกต้อง!",
|
|
10
|
+
incorrect: "✗ ไม่ถูกต้อง ลองอีกครั้ง!",
|
|
11
|
+
tryAgain: "ลองใหม่ ↺",
|
|
12
|
+
hint: "คำใบ้",
|
|
13
|
+
showHint: "ดูคำใบ้",
|
|
14
|
+
generatingKey: "กำลังสร้าง Developer VIP Key ให้คุณ...",
|
|
15
|
+
generatingDesc: "ระบบกำลังตรวจทานคำตอบทั้งหมด 15 ข้อและออก Key ให้...",
|
|
16
|
+
congratsTitle: "ยินดีด้วย! คุณคือ蜜橘ตัวจริง ",
|
|
17
|
+
congratsDesc: "ตอบถูกทั้ง 15 ข้อ! ปลดล็อค Developer VIP Tier 1,000 ครั้ง/นาที",
|
|
18
|
+
keyLabel: "Developer VIP API Key",
|
|
19
|
+
copyKey: "คัดลอก Key",
|
|
20
|
+
finishBtn: "เสร็จสิ้น",
|
|
21
|
+
errorTitle: "สร้าง Key ไม่สำเร็จ",
|
|
22
|
+
retryQuiz: "ลองใหม่",
|
|
23
|
+
keySaved: "Key ถูกบันทึกแล้ว ใช้ได้ทันที!",
|
|
24
|
+
noKey: "ยังไม่มี VIP Key ทำ /quiz เพื่อรับ Key",
|
|
25
|
+
keyCleared: "VIP Key ถูกลบแล้ว",
|
|
26
|
+
langPrompt: "เลือกภาษา / Choose language:",
|
|
27
|
+
answerPrompt: "ตอบ ❯ ",
|
|
28
|
+
answerPlaceholder: "A-D / 1-4 • H ดูคำใบ้ • Q ออก",
|
|
29
|
+
invalidAnswer: "กรุณาพิมพ์ A/B/C/D (หรือ 1-4), H เพื่อดูคำใบ้ หรือ Q เพื่อออก",
|
|
30
|
+
cancelled: "ออกจากแบบทดสอบแล้ว คะแนนและคำตอบในรอบนี้ไม่ถูกบันทึก",
|
|
31
|
+
},
|
|
32
|
+
en: {
|
|
33
|
+
title: "Ju Jingyi Miju Quiz Challenge ",
|
|
34
|
+
subtitle: "Answer all 15 questions correctly to claim a Developer VIP Key (1,000 req/min)",
|
|
35
|
+
questionOf: (idx, total) => `Question ${idx} / ${total}`,
|
|
36
|
+
score: (score, total) => `Score: ${score} / ${total}`,
|
|
37
|
+
correct: " Correct!",
|
|
38
|
+
incorrect: "✗ Incorrect, try again!",
|
|
39
|
+
tryAgain: "Try Again ↺",
|
|
40
|
+
hint: "Hint",
|
|
41
|
+
showHint: "Show Hint",
|
|
42
|
+
generatingKey: "Generating your Developer VIP Key...",
|
|
43
|
+
generatingDesc: "Verifying your perfect score and issuing your exclusive key...",
|
|
44
|
+
congratsTitle: "Congratulations! Official Miju ",
|
|
45
|
+
congratsDesc: "All 15 answers correct! Unlocked Developer VIP Tier (1,000 req/min).",
|
|
46
|
+
keyLabel: "Developer VIP API Key",
|
|
47
|
+
copyKey: "Copy Key",
|
|
48
|
+
finishBtn: "Done",
|
|
49
|
+
errorTitle: "Key Generation Failed",
|
|
50
|
+
retryQuiz: "Try Again",
|
|
51
|
+
keySaved: "Key saved and ready to use!",
|
|
52
|
+
noKey: "No VIP Key yet. Run /quiz to claim one.",
|
|
53
|
+
keyCleared: "VIP Key has been cleared.",
|
|
54
|
+
langPrompt: "Choose language / เลือกภาษา:",
|
|
55
|
+
answerPrompt: "Answer ❯ ",
|
|
56
|
+
answerPlaceholder: "A-D / 1-4 • H hint • Q quit",
|
|
57
|
+
invalidAnswer: "Type A/B/C/D (or 1-4), H for a hint, or Q to quit.",
|
|
58
|
+
cancelled: "Quiz cancelled. This attempt was not saved.",
|
|
59
|
+
},
|
|
60
|
+
zh: {
|
|
61
|
+
title: "鞠婧祎蜜橘专属问答挑战 ",
|
|
62
|
+
subtitle: "正确回答全部15道题目即可获取开发者 VIP Key (1,000次/分钟)",
|
|
63
|
+
questionOf: (idx, total) => `第 ${idx} 题 / 共 ${total} 题`,
|
|
64
|
+
score: (score, total) => `得分: ${score} / ${total}`,
|
|
65
|
+
correct: " 回答正确!",
|
|
66
|
+
incorrect: "✗ 答错啦,继续加油!",
|
|
67
|
+
tryAgain: "重新作答 ↺",
|
|
68
|
+
hint: "提示",
|
|
69
|
+
showHint: "查看提示",
|
|
70
|
+
generatingKey: "正在为您生成专属开发者 VIP Key...",
|
|
71
|
+
generatingDesc: "系统正在验证满分答卷并颁发专属权限...",
|
|
72
|
+
congratsTitle: "恭喜通关!资深蜜橘 ",
|
|
73
|
+
congratsDesc: "答对全部15道题目!成功解锁 Developer VIP Tier 1,000次/分钟。",
|
|
74
|
+
keyLabel: "开发者 VIP API Key",
|
|
75
|
+
copyKey: "复制密钥",
|
|
76
|
+
finishBtn: "完成",
|
|
77
|
+
errorTitle: "密钥生成失败",
|
|
78
|
+
retryQuiz: "重新尝试",
|
|
79
|
+
keySaved: "Key 已保存,可以立即使用!",
|
|
80
|
+
noKey: "还没有 VIP Key,运行 /quiz 来获取。",
|
|
81
|
+
keyCleared: "VIP Key 已清除。",
|
|
82
|
+
langPrompt: "选择语言 / Choose language:",
|
|
83
|
+
answerPrompt: "回答 ❯ ",
|
|
84
|
+
answerPlaceholder: "A-D / 1-4 • H 提示 • Q 退出",
|
|
85
|
+
invalidAnswer: "请输入 A/B/C/D(或 1-4),输入 H 查看提示,或输入 Q 退出。",
|
|
86
|
+
cancelled: "已退出问答,本次成绩和答案未保存。",
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const QUIZ_QUESTIONS = [
|
|
91
|
+
{
|
|
92
|
+
q: {
|
|
93
|
+
th: "1. วันเกิดของจวีจิ้งอี (Ju Jingyi) คือวันใด?",
|
|
94
|
+
en: "1. What is Ju Jingyi's date of birth?",
|
|
95
|
+
zh: "1. 鞠婧祎的出生日期是哪一天?"
|
|
96
|
+
},
|
|
97
|
+
options: {
|
|
98
|
+
th: ["18 มิถุนายน 1994", "23 กรกฎาคม 1995", "4 มีนาคม 1993", "12 กันยายน 1996"],
|
|
99
|
+
en: ["June 18, 1994", "July 23, 1995", "March 4, 1993", "September 12, 1996"],
|
|
100
|
+
zh: ["1994年6月18日", "1995年7月23日", "1993年3月4日", "1996年9月12日"]
|
|
101
|
+
},
|
|
102
|
+
answer: 0,
|
|
103
|
+
hint: {
|
|
104
|
+
th: "เธอเกิดในช่วงกลางปี ค.ศ. 1994 ในจีน ลองนึกถึงเดือนที่อยู่กึ่งกลางของปี",
|
|
105
|
+
en: "She was born in mid-1994 in China — think of the month that sits near the midpoint of the calendar year.",
|
|
106
|
+
zh: "她出生于1994年,月份大约在全年中段,仔细想想是哪一天。"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
q: {
|
|
111
|
+
th: "2. จวีจิ้งอีเกิดที่เมืองและมณฑลใดในประเทศจีน?",
|
|
112
|
+
en: "2. In which city and province of China was Ju Jingyi born?",
|
|
113
|
+
zh: "2. 鞠婧祎出生于中国的哪座城市?"
|
|
114
|
+
},
|
|
115
|
+
options: {
|
|
116
|
+
th: ["เมืองสุยหนิง มณฑลเสฉวน (Suining, Sichuan)", "เมืองเฉิงตู มณฑลเสฉวน", "เมืองหางโจว มณฑลเจ้อเจียง", "เทศบาลนครเซี่ยงไฮ้"],
|
|
117
|
+
en: ["Suining, Sichuan Province", "Chengdu, Sichuan Province", "Hangzhou, Zhejiang Province", "Shanghai Municipality"],
|
|
118
|
+
zh: ["四川省遂宁市", "四川省成都市", "浙江省杭州市", "上海市"]
|
|
119
|
+
},
|
|
120
|
+
answer: 0,
|
|
121
|
+
hint: {
|
|
122
|
+
th: "บ้านเกิดของเธออยู่ในมณฑลที่ขึ้นชื่อด้านอาหารรสเผ็ด หมีแพนด้า และเมืองหลวงของมณฑลคือเฉิงตู แต่เธอเกิดในเมืองที่เล็กกว่า",
|
|
123
|
+
en: "Her hometown province is known for spicy food and giant pandas — its capital is Chengdu, but she was born in a smaller city there.",
|
|
124
|
+
zh: "她家乡所在省份以麻辣美食和大熊猫闻名,省会是成都,但她出生在省内另一座较小的城市。"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
q: {
|
|
129
|
+
th: "3. ชื่อแฟนคลับอย่างเป็นทางการ (Official Fandom Name) ของจวีจิ้งอีคืออะไร?",
|
|
130
|
+
en: "3. What is the official fandom name of Ju Jingyi?",
|
|
131
|
+
zh: "3. 鞠婧祎的官方粉丝名是什么?"
|
|
132
|
+
},
|
|
133
|
+
options: {
|
|
134
|
+
th: ["มี่จวี (Miju / 蜜橘 - ส้มหวาน)", "เสี่ยวเหมย (Xiaomei)", "ชิงเฉิง (Qingcheng)", "ไป๋เหอ (Baihe)"],
|
|
135
|
+
en: ["Miju (蜜橘 - Sweet Tangerine)", "Xiaomei", "Qingcheng", "Baihe"],
|
|
136
|
+
zh: ["蜜橘 (Miju)", "小妹 (Xiaomei)", "倾城 (Qingcheng)", "百合 (Baihe)"]
|
|
137
|
+
},
|
|
138
|
+
answer: 0,
|
|
139
|
+
hint: {
|
|
140
|
+
th: "ชื่อแฟนด้อมเป็นชื่อผลไม้สีส้มขนาดเล็ก รสหวานเปรี้ยว นิยมรับประทานในเทศกาลฤดูหนาว",
|
|
141
|
+
en: "The fandom name is a small sweet citrus fruit with orange skin, often enjoyed during winter festivals in East Asia.",
|
|
142
|
+
zh: "粉丝名是一种小巧甘甜的橙色柑橘类水果,东亚冬季节日中常见。"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
q: {
|
|
147
|
+
th: "4. สีประจำตัวและสีทางการของแฟนคลับ (Official Support Colors) มีสีอะไรบ้าง?",
|
|
148
|
+
en: "4. What are the official fan support colors of Ju Jingyi?",
|
|
149
|
+
zh: "4. 鞠婧祎与蜜橘的官方应援色是哪些?"
|
|
150
|
+
},
|
|
151
|
+
options: {
|
|
152
|
+
th: ["น้ำเงินฟ้าคราม (Sky Blue), ชมพูดอกซากุระ (Sakura Pink), และทองประกาย (Gold)", "แดงทับทิม, เขียวมรกต, ม่วงลาเวนเดอร์", "เหลืองมะนาว, ส้มพีช, ขาวไข่มุก", "ดำสนิท, เงินเมทัลลิก, ฟ้าพาสเทล"],
|
|
153
|
+
en: ["Sky Blue (蔚蓝色), Sakura Pink (樱花粉), and Shining Gold (耀金)", "Ruby Red, Emerald Green, Lavender Purple", "Lemon Yellow, Peach Orange, Pearl White", "Jet Black, Metallic Silver, Pastel Blue"],
|
|
154
|
+
zh: ["蔚蓝色、樱花粉、耀金", "红宝石色、翡翠绿、薰衣草紫", "柠檬黄、蜜桃橙、珍珠白", "曜石黑、金属银、浅天蓝"]
|
|
155
|
+
},
|
|
156
|
+
answer: 0,
|
|
157
|
+
hint: {
|
|
158
|
+
th: "สีทั้งสามนำแรงบันดาลใจมาจากท้องฟ้า ดอกไม้ยามใบไม้ผลิ และแสงสว่างของดวงอาทิตย์",
|
|
159
|
+
en: "The three colors draw inspiration from the sky, spring blossoms, and the radiance of sunlight.",
|
|
160
|
+
zh: "三种颜色分别灵感来自晴朗的天空、春日的花朵以及阳光般的光辉。"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
q: {
|
|
165
|
+
th: "5. จวีจิ้งอีสร้างประวัติศาสตร์ในวง SNH48 อย่างไร?",
|
|
166
|
+
en: "5. What historic milestone did Ju Jingyi achieve during her SNH48 career?",
|
|
167
|
+
zh: "5. 鞠婧祎在SNH48总选举中创造了怎样的历史纪录?"
|
|
168
|
+
},
|
|
169
|
+
options: {
|
|
170
|
+
th: ["ชนะการเลือกตั้งอันดับ 1 สองปีซ้อน (2016 & 2017) และเข้าสู่ Star Hall of Fame", "เป็นกัปตันทีมคนแรกของวง SNH48", "มีผลงานเพลงมากที่สุดในวงถึง 100 เพลง", "ก่อตั้งวงสาขาย่อยในปักกิ่ง"],
|
|
171
|
+
en: ["Won 1st Place in General Elections twice in a row (2016 & 2017) and promoted to Star Hall of Fame", "Was the first overall team captain of SNH48", "Released the highest number of singles in group history (100 songs)", "Founded the Beijing sub-unit"],
|
|
172
|
+
zh: ["蝉联第三届与第四届总选举冠军并晋升SNH48明星殿堂", "成为SNH48全团第一任总队长", "在团期间发行了100首单曲", "创立了北京姐妹团"]
|
|
173
|
+
},
|
|
174
|
+
answer: 0,
|
|
175
|
+
hint: {
|
|
176
|
+
th: "เธอเป็นสมาชิกคนเดียวที่ครองอันดับหนึ่งต่อเนื่องสองสมัย จนได้รับสิทธิ์พิเศษสูงสุดในวง",
|
|
177
|
+
en: "She is the only member to top the annual election rankings in two consecutive years, earning the highest honor tier.",
|
|
178
|
+
zh: "她是唯一连续两届年度总选登顶的成员,因此获得了团队最高荣誉资格。"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
q: {
|
|
183
|
+
th: "6. ผลงานซีรีส์แนวย้อนยุคที่สร้างชื่อเสียงและเป็นผลงานแจ้งเกิดระดับตำนานของจวีจิ้งอีคือเรื่องใด?",
|
|
184
|
+
en: "6. Which classic historical costume drama served as Ju Jingyi's breakthrough breakout role?",
|
|
185
|
+
zh: "6. 哪部经典古装剧是鞠婧祎经典代表作并让她一举成名?"
|
|
186
|
+
},
|
|
187
|
+
options: {
|
|
188
|
+
th: ["ตำนานหยุนซี มเหสียอดอัจฉริยะแห่งพิษ (Legend of Yunxi - 芸汐传)", "กรุ่นรักกลิ่นบุปผา (The Blooms at Ruyi Pavilion)", "ปรมาจารย์ลัทธิมาร (The Untamed)", "สามชาติสามภพ ป่าท้อสิบหลี่ (Eternal Love)"],
|
|
189
|
+
en: ["Legend of Yunxi (芸汐传 - 2018)", "The Blooms at Ruyi Pavilion (如意芳霏)", "The Untamed (陈情令)", "Eternal Love (三生三世十里桃花)"],
|
|
190
|
+
zh: ["《芸汐传》(Legend of Yunxi)", "《如意芳霏》(The Blooms at Ruyi Pavilion)", "《陈情令》(The Untamed)", "《三生三世十里桃花》(Eternal Love)"]
|
|
191
|
+
},
|
|
192
|
+
answer: 0,
|
|
193
|
+
hint: {
|
|
194
|
+
th: "ซีรีส์แจ้งเกิดปี 2018 เล่าเรื่องหญิงสาวอัจฉริยะด้านพิษในราชสำนัก ชื่อเรื่องตั้งตามชื่อตัวละครเอก",
|
|
195
|
+
en: "This 2018 drama features a brilliant young woman skilled in poisons at an imperial court — the title is named after her character.",
|
|
196
|
+
zh: "这部2018年古装剧讲述一位精通毒术的聪慧女子,剧名取自女主角的名字。"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
q: {
|
|
201
|
+
th: "7. ในซีรีส์ยอดฮิต 'บุปผารักอลวน (In Blossom - 2024)' จวีจิ้งอีรับบทเป็นตัวละครใด?",
|
|
202
|
+
en: "7. In the 2024 hit mystery drama 'In Blossom', which character(s) did Ju Jingyi portray?",
|
|
203
|
+
zh: "7. 在2024年爆款古装悬疑剧《花间令》中,鞠婧祎饰演的角色是?"
|
|
204
|
+
},
|
|
205
|
+
options: {
|
|
206
|
+
th: ["หยางไฉ่เวย / ซ่างกวนจื่อ (Yang Caiwei / Shangguan Zhi)", "หานอวิ๋นซี / ซื่อเฉียง", "ไป๋ซู่เจิน / เสี่ยวชิง", "ฟู่หรง / ชุยหว่าน"],
|
|
207
|
+
en: ["Yang Caiwei / Shangguan Zhi (Dual Personas)", "Han Yunxi / Si Qiang", "Bai Suzhen / Xiaoqing", "Fu Rong / Cui Wan"],
|
|
208
|
+
zh: ["杨采薇 / 上官芷 (一人分饰两角)", "韩芸汐 / 四娘", "白素贞 / 小青", "傅容 / 崔绾"]
|
|
209
|
+
},
|
|
210
|
+
answer: 0,
|
|
211
|
+
hint: {
|
|
212
|
+
th: "เธอรับบท 2 ตัวละครในเรื่องเดียว นิสัยตรงข้ามกันสิ้นเชิง — หนึ่งใจดี อีกหนึ่งเจ้าเล่ห์",
|
|
213
|
+
en: "She played two characters in the same drama with opposite personalities — one gentle, one scheming.",
|
|
214
|
+
zh: "她在剧中同时饰演两个性格截然相反的角色,一善一奸。"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
q: {
|
|
219
|
+
th: "8. เครื่องดนตรีที่จวีจิ้งอีมีความเชี่ยวชาญระดับสูงและฝึกฝนมาตั้งแต่เด็กคือเครื่องดนตรีใด?",
|
|
220
|
+
en: "8. Which musical instruments is Ju Jingyi highly proficient in and has played since childhood?",
|
|
221
|
+
zh: "8. 鞠婧祎从小精通并多次在舞台上惊艳演奏的乐器是?"
|
|
222
|
+
},
|
|
223
|
+
options: {
|
|
224
|
+
th: ["ไวโอลิน (Violin) และ กู่เจิง (Guzheng)", "กลองชุด และ เบส", "ทรัมเป็ต และ แซกโซโฟน", "ฮาร์ป และ ฟลุต"],
|
|
225
|
+
en: ["Violin and Guzheng (Chinese Zither)", "Drum Set and Electric Bass", "Trumpet and Saxophone", "Harp and Flute"],
|
|
226
|
+
zh: ["小提琴与古筝", "架子鼓与贝斯", "小号与萨克斯", "竖琴与长笛"]
|
|
227
|
+
},
|
|
228
|
+
answer: 0,
|
|
229
|
+
hint: {
|
|
230
|
+
th: "เธอเล่นเครื่องดนตรีสายชนิดหนึ่งจากตะวันตก และอีกชนิดจากเอเชียตะวันออก ทั้งคู่ใช้สายเป็นแหล่งเสียง",
|
|
231
|
+
en: "She plays one Western string instrument and one traditional East Asian string instrument.",
|
|
232
|
+
zh: "她精通一件西方弦乐器和一件东亚传统弦乐器,均以弦为发声方式。"
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
q: {
|
|
237
|
+
th: "9. แบรนด์แฟชั่นระดับไฮเอนด์ระดับโลกแบรนด์ใดที่แต่งตั้งจวีจิ้งอีเป็น Global Brand Ambassador?",
|
|
238
|
+
en: "9. Which world-renowned international luxury fashion house appointed Ju Jingyi as its Global Brand Ambassador?",
|
|
239
|
+
zh: "9. 哪家世界顶级奢华时尚品牌官宣鞠婧祎为全球品牌代言人?"
|
|
240
|
+
},
|
|
241
|
+
options: {
|
|
242
|
+
th: ["Versace (เวอร์ซาเช่)", "Gucci (กุชชี)", "Chanel (ชาเนล)", "Dior (ดิออร์)"],
|
|
243
|
+
en: ["Versace", "Gucci", "Chanel", "Dior"],
|
|
244
|
+
zh: ["范思哲 (Versace)", "古驰 (Gucci)", "香奈儿 (Chanel)", "迪奥 (Dior)"]
|
|
245
|
+
},
|
|
246
|
+
answer: 0,
|
|
247
|
+
hint: {
|
|
248
|
+
th: "แบรนด์นี้มาจากอิตาลี ใช้หัวเมดูซ่าเป็นสัญลักษณ์ และมีชื่อเสียงด้านแฟชั่นสุดหรู",
|
|
249
|
+
en: "This Italian luxury brand is famous for its Medusa head emblem and bold, opulent designs.",
|
|
250
|
+
zh: "这个来自意大利的奢侈品牌以神话中的美杜莎头像为标志,以大胆华丽的设计著称。"
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
q: {
|
|
255
|
+
th: "10. ซิงเกิลเดี่ยวเพลงหลักล่าสุดประจำปี 2026 ของจวีจิ้งอีมีชื่อว่าอะไร?",
|
|
256
|
+
en: "10. What is the title of Ju Jingyi's latest 2026 flagship solo single?",
|
|
257
|
+
zh: "10. 鞠婧祎2026年最新发布的个人主打单曲名称是?"
|
|
258
|
+
},
|
|
259
|
+
options: {
|
|
260
|
+
th: ["FIND THE WAY", "Rain Love (雨爱)", "Everyday", "Don't Touch"],
|
|
261
|
+
en: ["FIND THE WAY", "Rain Love (雨爱)", "Everyday", "Don't Touch"],
|
|
262
|
+
zh: ["《FIND THE WAY》", "《雨爱》(Rain Love)", "《日日夜夜》(Everyday)", "《Don't Touch》"]
|
|
263
|
+
},
|
|
264
|
+
answer: 0,
|
|
265
|
+
hint: {
|
|
266
|
+
th: "ชื่อเพลงปี 2026 เป็นภาษาอังกฤษ 3 คำ สื่อถึงการค้นหาทิศทางในชีวิต",
|
|
267
|
+
en: "The 2026 single has a 3-word English title about searching for direction in life.",
|
|
268
|
+
zh: "这首2026年单曲的名称由三个英文单词组成,传达寻找人生方向的主题。"
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
q: {
|
|
273
|
+
th: "11. ชื่อเล่นและฉายาที่แฟนๆ และคนทั่วไปเรียกจวีจิ้งอีด้วยความรักคืออะไร?",
|
|
274
|
+
en: "11. What is the most affectionate nickname by which fans and the public refer to Ju Jingyi?",
|
|
275
|
+
zh: "11. 粉丝与大众最常亲切称呼鞠婧祎的昵称是?"
|
|
276
|
+
},
|
|
277
|
+
options: {
|
|
278
|
+
th: ["เสี่ยวจวี (Xiao Ju / 小鞠) หรือ Kiku", "จวีเอ๋อร์ (Ju'er)", "อาอี (A-Yi)", "จิ้งจิ้ง (JingJing)"],
|
|
279
|
+
en: ["Xiao Ju (小鞠) / Kiku", "Ju'er", "A-Yi", "JingJing"],
|
|
280
|
+
zh: ["小鞠 (Xiao Ju) / Kiku", "鞠儿", "阿祎", "婧婧"]
|
|
281
|
+
},
|
|
282
|
+
answer: 0,
|
|
283
|
+
hint: {
|
|
284
|
+
th: "ชื่อเล่นนี้ดัดแปลงมาจากนามสกุลจีนของเธอ และออกเสียงคล้ายกับชื่อในภาษาญี่ปุ่น",
|
|
285
|
+
en: "The nickname is derived from her Chinese surname and sounds similar to a Japanese given name.",
|
|
286
|
+
zh: "这个昵称源自她的中文姓氏,发音也与一个常见的日文名字相近。"
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
q: {
|
|
291
|
+
th: "12. เพลงประกอบซีรีส์ 'บุปผารักอลวน (In Blossom OST)' ที่จวีจิ้งอีขับร้องมีชื่อว่าอะไร?",
|
|
292
|
+
en: "12. What is the title of the original soundtrack (OST) song performed by Ju Jingyi for 'In Blossom'?",
|
|
293
|
+
zh: "12. 鞠婧祎为电视剧《花间令》献唱的主题曲/插曲是?"
|
|
294
|
+
},
|
|
295
|
+
options: {
|
|
296
|
+
th: ["ดั่งแพรไหม (如纱 - Ru Sha / Like a Silk)", "รักแรกพบ (First Love)", "แสงสว่างในสายลม", "บุปผาร่วงโรย"],
|
|
297
|
+
en: ["Like a Silk (如纱 - Ru Sha)", "First Love", "Light in the Wind", "Fallen Blossoms"],
|
|
298
|
+
zh: ["《如纱》(Ru Sha / Like a Silk)", "《初恋》", "《风中之光》", "《落花》"]
|
|
299
|
+
},
|
|
300
|
+
answer: 0,
|
|
301
|
+
hint: {
|
|
302
|
+
th: "ชื่อเพลงเป็นอักษรจีน 2 ตัว สื่อถึงสิ่งบางเบาโปร่งแสง เหมือนผ้าที่ปลิวตามสายลม",
|
|
303
|
+
en: "The song title is two Chinese characters evoking something light and translucent, like fabric in the breeze.",
|
|
304
|
+
zh: "歌名由两个汉字组成,意境轻盈透明,如随风飘动的薄纱。"
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
q: {
|
|
309
|
+
th: "13. สัตว์เลี้ยงตัวโปรดของจวีจิ้งอีคือสัตว์ชนิดใดและมีชื่อว่าอะไร?",
|
|
310
|
+
en: "13. What pet does Ju Jingyi cherish, and what is its name?",
|
|
311
|
+
zh: "13. 鞠婧祎心爱的宠物猫咪名字叫什么?"
|
|
312
|
+
},
|
|
313
|
+
options: {
|
|
314
|
+
th: ["แมว ชื่อว่า 'ฮาฮา' (Haha / 哈哈)", "สุนัข ชื่อว่า 'พุดดิ้ง'", "กระต่าย ชื่อว่า 'เสี่ยวไป๋'", "นกแก้ว ชื่อว่า 'กีกี้'"],
|
|
315
|
+
en: ["A cat named 'Haha' (哈哈)", "A puppy named 'Pudding'", "A rabbit named 'Xiaobai'", "A parrot named 'Kiki'"],
|
|
316
|
+
zh: ['猫咪,名字叫\u201C哈哈\u201D', '小狗,名字叫\u201C布丁\u201D', '小兔,名字叫\u201C小白\u201D', '鹦鹉,名字叫\u201C琪琪\u201D']
|
|
317
|
+
},
|
|
318
|
+
answer: 0,
|
|
319
|
+
hint: {
|
|
320
|
+
th: "สัตว์เลี้ยงของเธอเป็นแมว และชื่อของมันฟังดูเหมือนเสียงหัวเราะในหลายภาษา",
|
|
321
|
+
en: "Her pet is a cat whose name sounds like the onomatopoeia for laughter in many languages.",
|
|
322
|
+
zh: "她养的是一只猫咪,它的名字读起来像是笑声的拟声词。"
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
q: {
|
|
327
|
+
th: "14. ในซีรีส์ 'ตำนานนางพญางูขาว (The Legend of White Snake - 2019)' จวีจิ้งอีรับบทเป็นใคร?",
|
|
328
|
+
en: "14. In the 2019 TV series 'The Legend of White Snake', which iconic role did Ju Jingyi star as?",
|
|
329
|
+
zh: "14. 在2019版电视剧《新白娘子传奇》中,鞠婧祎领衔主演的角色是?"
|
|
330
|
+
},
|
|
331
|
+
options: {
|
|
332
|
+
th: ["ไป๋ซู่เจิน (Bai Suzhen)", "เสี่ยวชิง (Xiaoqing)", "หลินไต้อวี้ (Lin Daiyu)", "ซูเสี่ยวเม่ย"],
|
|
333
|
+
en: ["Bai Suzhen (Lady White Snake)", "Xiaoqing (Green Snake)", "Lin Daiyu", "Su Xiaomei"],
|
|
334
|
+
zh: ["白素贞 (Bai Suzhen)", "小青 (Xiaoqing)", "林黛玉", "苏小妹"]
|
|
335
|
+
},
|
|
336
|
+
answer: 0,
|
|
337
|
+
hint: {
|
|
338
|
+
th: "ตัวละครที่เธอรับบทเป็นตัวเอกหญิงจากตำนานจีนคลาสสิก ชื่อตัวละครอ้างอิงถึงงูสีขาว",
|
|
339
|
+
en: "The character she portrayed is the female lead of a classic Chinese legend — her name references a white serpent.",
|
|
340
|
+
zh: "她饰演的是中国经典民间传说的女主角,角色名与一条白蛇有关。"
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
q: {
|
|
345
|
+
th: "15. คติประจำใจและปรัชญาการทำงานที่มีชื่อเสียงของจวีจิ้งอีคือข้อใด?",
|
|
346
|
+
en: "15. What is Ju Jingyi's renowned life philosophy and guiding motto?",
|
|
347
|
+
zh: "15. 鞠婧祎广为人知的处世金句与座右铭是哪一句?"
|
|
348
|
+
},
|
|
349
|
+
options: {
|
|
350
|
+
th: ["认真工作,踏实生活 (ตั้งใจทำงาน ใช้ชีวิตอย่างมั่นคงและก้าวไปข้างหน้าอย่างจริงใจ)", "อย่าหยุดเมื่อเหนื่อย แต่หยุดเมื่อชนะ", "ความสำเร็จอยู่ที่การรอคอย", "ความสมบูรณ์แบบไม่มีอยู่จริง"],
|
|
351
|
+
en: ["Work hard with dedication, live life grounded and sincerely (认真工作,踏实生活)", "Don't stop when tired, stop when victorious", "Success comes to those who wait", "Perfection does not exist"],
|
|
352
|
+
zh: ['\u201C认真工作,踏实生活\u201D', '\u201C不要在累的时候停下,要在成功时停下\u201D', '\u201C成功在于等待\u201D', '\u201C完美并不存在\u201D']
|
|
353
|
+
},
|
|
354
|
+
answer: 0,
|
|
355
|
+
hint: {
|
|
356
|
+
th: "คติของเธอเป็นประโยคจีน 8 ตัวอักษร แบ่งเป็นสองส่วน พูดถึงการทำงานและการใช้ชีวิตอย่างจริงใจ",
|
|
357
|
+
en: "Her motto is an eight-character Chinese phrase, split into two parts — one about work, one about living sincerely.",
|
|
358
|
+
zh: "她的座右铭是一句八字短语,分两句,分别谈及工作态度与生活方式。"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
export const LETTER_LABELS = ['A', 'B', 'C', 'D'];
|
|
364
|
+
|
|
365
|
+
export function shuffleQuizQuestions(questions) {
|
|
366
|
+
return questions.map(q => {
|
|
367
|
+
const order = [0, 1, 2, 3];
|
|
368
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
369
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
370
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
q: q.q,
|
|
374
|
+
hint: q.hint,
|
|
375
|
+
options: {
|
|
376
|
+
th: order.map(idx => q.options.th[idx]),
|
|
377
|
+
en: order.map(idx => q.options.en[idx]),
|
|
378
|
+
zh: order.map(idx => q.options.zh[idx])
|
|
379
|
+
},
|
|
380
|
+
optionOrder: [...order],
|
|
381
|
+
answer: order.indexOf(q.answer)
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import jpeg from 'jpeg-js';
|
|
5
|
+
|
|
6
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export const DEFAULT_IMAGE_DIRECTORY = path.resolve(moduleDirectory, '../../src/assets/images');
|
|
8
|
+
|
|
9
|
+
const RESET = '\x1b[0m';
|
|
10
|
+
const foreground = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
|
11
|
+
const background = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
|
|
12
|
+
const QUADRANT_GLYPHS = [
|
|
13
|
+
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛',
|
|
14
|
+
'▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function supportsTerminalImages({
|
|
18
|
+
env = process.env,
|
|
19
|
+
isTTY = process.stdout.isTTY,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const setting = String(env.KIKU_TUI_IMAGES || '').trim().toLowerCase();
|
|
22
|
+
if (['0', 'false', 'off', 'no'].includes(setting)) return false;
|
|
23
|
+
if (['1', 'true', 'on', 'yes'].includes(setting)) return true;
|
|
24
|
+
if (!isTTY || env.NO_COLOR !== undefined || String(env.TERM || '').toLowerCase() === 'dumb') return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const clampChannel = value => Math.max(0, Math.min(255, Math.round(value)));
|
|
29
|
+
|
|
30
|
+
function sourceChannel(image, x, y, channel) {
|
|
31
|
+
const safeX = Math.max(0, Math.min(image.width - 1, x));
|
|
32
|
+
const safeY = Math.max(0, Math.min(image.height - 1, y));
|
|
33
|
+
return image.data[(safeY * image.width + safeX) * 4 + channel];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sampleBilinear(image, x, y, targetWidth, targetHeight) {
|
|
37
|
+
const sourceX = ((x + 0.5) * image.width) / targetWidth - 0.5;
|
|
38
|
+
const sourceY = ((y + 0.5) * image.height) / targetHeight - 0.5;
|
|
39
|
+
const left = Math.floor(sourceX);
|
|
40
|
+
const top = Math.floor(sourceY);
|
|
41
|
+
const right = left + 1;
|
|
42
|
+
const bottom = top + 1;
|
|
43
|
+
const mixX = sourceX - left;
|
|
44
|
+
const mixY = sourceY - top;
|
|
45
|
+
const result = [];
|
|
46
|
+
|
|
47
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
48
|
+
const topMix = sourceChannel(image, left, top, channel) * (1 - mixX)
|
|
49
|
+
+ sourceChannel(image, right, top, channel) * mixX;
|
|
50
|
+
const bottomMix = sourceChannel(image, left, bottom, channel) * (1 - mixX)
|
|
51
|
+
+ sourceChannel(image, right, bottom, channel) * mixX;
|
|
52
|
+
result.push(topMix * (1 - mixY) + bottomMix * mixY);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resizeAndEnhance(image, targetWidth, targetHeight) {
|
|
58
|
+
const pixels = new Float32Array(targetWidth * targetHeight * 3);
|
|
59
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
60
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
61
|
+
const color = sampleBilinear(image, x, y, targetWidth, targetHeight);
|
|
62
|
+
const offset = (y * targetWidth + x) * 3;
|
|
63
|
+
pixels[offset] = color[0];
|
|
64
|
+
pixels[offset + 1] = color[1];
|
|
65
|
+
pixels[offset + 2] = color[2];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const enhanced = new Uint8Array(pixels.length);
|
|
70
|
+
const at = (x, y, channel) => {
|
|
71
|
+
const safeX = Math.max(0, Math.min(targetWidth - 1, x));
|
|
72
|
+
const safeY = Math.max(0, Math.min(targetHeight - 1, y));
|
|
73
|
+
return pixels[(safeY * targetWidth + safeX) * 3 + channel];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
77
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
78
|
+
const offset = (y * targetWidth + x) * 3;
|
|
79
|
+
const sharpened = [0, 1, 2].map(channel => {
|
|
80
|
+
const center = at(x, y, channel);
|
|
81
|
+
const neighbourAverage = (
|
|
82
|
+
at(x - 1, y, channel) + at(x + 1, y, channel)
|
|
83
|
+
+ at(x, y - 1, channel) + at(x, y + 1, channel)
|
|
84
|
+
) / 4;
|
|
85
|
+
return center * 1.18 - neighbourAverage * 0.18;
|
|
86
|
+
});
|
|
87
|
+
const luminance = sharpened[0] * 0.2126 + sharpened[1] * 0.7152 + sharpened[2] * 0.0722;
|
|
88
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
89
|
+
const saturated = luminance + (sharpened[channel] - luminance) * 1.06;
|
|
90
|
+
enhanced[offset + channel] = clampChannel((saturated - 128) * 1.03 + 128);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return enhanced;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function colorDistance(left, right) {
|
|
98
|
+
const red = left[0] - right[0];
|
|
99
|
+
const green = left[1] - right[1];
|
|
100
|
+
const blue = left[2] - right[2];
|
|
101
|
+
return red * red + green * green + blue * blue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function averageColors(colors, fallback) {
|
|
105
|
+
if (colors.length === 0) return fallback;
|
|
106
|
+
const sum = colors.reduce((total, color) => [
|
|
107
|
+
total[0] + color[0],
|
|
108
|
+
total[1] + color[1],
|
|
109
|
+
total[2] + color[2],
|
|
110
|
+
], [0, 0, 0]);
|
|
111
|
+
return sum.map(value => clampChannel(value / colors.length));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function encodeQuadrants(colors) {
|
|
115
|
+
let first = colors[0];
|
|
116
|
+
let second = colors[1];
|
|
117
|
+
let furthestDistance = -1;
|
|
118
|
+
for (let left = 0; left < colors.length; left++) {
|
|
119
|
+
for (let right = left + 1; right < colors.length; right++) {
|
|
120
|
+
const distance = colorDistance(colors[left], colors[right]);
|
|
121
|
+
if (distance > furthestDistance) {
|
|
122
|
+
furthestDistance = distance;
|
|
123
|
+
first = colors[left];
|
|
124
|
+
second = colors[right];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let assignments = [0, 0, 0, 0];
|
|
130
|
+
for (let iteration = 0; iteration < 3; iteration++) {
|
|
131
|
+
assignments = colors.map(color => colorDistance(color, second) < colorDistance(color, first) ? 1 : 0);
|
|
132
|
+
first = averageColors(colors.filter((_, index) => assignments[index] === 0), first);
|
|
133
|
+
second = averageColors(colors.filter((_, index) => assignments[index] === 1), second);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mask = assignments.reduce((value, assignment, index) => value | (assignment << index), 0);
|
|
137
|
+
return {
|
|
138
|
+
glyph: QUADRANT_GLYPHS[mask],
|
|
139
|
+
foregroundColor: second,
|
|
140
|
+
backgroundColor: first,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderJpegToAnsi(buffer, {
|
|
145
|
+
columns = process.stdout.columns || 80,
|
|
146
|
+
rows = process.stdout.rows || 24,
|
|
147
|
+
maxWidth = 60,
|
|
148
|
+
maxRows = 20,
|
|
149
|
+
indent = ' ',
|
|
150
|
+
} = {}) {
|
|
151
|
+
const image = jpeg.decode(buffer, { useTArray: true, formatAsRGBA: true });
|
|
152
|
+
if (!image?.width || !image?.height) throw new Error('Unable to decode gallery image');
|
|
153
|
+
|
|
154
|
+
const indentWidth = Array.from(indent).length;
|
|
155
|
+
const availableWidth = Math.max(8, Math.floor(columns) - indentWidth - 2);
|
|
156
|
+
let targetWidth = Math.max(8, Math.min(Math.floor(maxWidth), availableWidth, image.width));
|
|
157
|
+
const availableRows = Math.max(3, Math.min(Math.floor(maxRows), Math.floor(rows) - 7));
|
|
158
|
+
let targetPixelHeight = Math.max(2, Math.round((image.height / image.width) * targetWidth));
|
|
159
|
+
let targetRows = Math.ceil(targetPixelHeight / 2);
|
|
160
|
+
|
|
161
|
+
if (targetRows > availableRows) {
|
|
162
|
+
targetRows = availableRows;
|
|
163
|
+
targetPixelHeight = targetRows * 2;
|
|
164
|
+
targetWidth = Math.max(8, Math.min(targetWidth, Math.round((image.width / image.height) * targetPixelHeight)));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sampledWidth = targetWidth * 2;
|
|
168
|
+
const resized = resizeAndEnhance(image, sampledWidth, targetPixelHeight);
|
|
169
|
+
const pixelAt = (x, y) => {
|
|
170
|
+
const offset = (y * sampledWidth + x) * 3;
|
|
171
|
+
return [resized[offset], resized[offset + 1], resized[offset + 2]];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const lines = [];
|
|
175
|
+
for (let row = 0; row < targetRows; row++) {
|
|
176
|
+
let line = indent;
|
|
177
|
+
const topY = Math.min(targetPixelHeight - 1, row * 2);
|
|
178
|
+
const bottomY = Math.min(targetPixelHeight - 1, row * 2 + 1);
|
|
179
|
+
for (let column = 0; column < targetWidth; column++) {
|
|
180
|
+
const leftX = column * 2;
|
|
181
|
+
const rightX = leftX + 1;
|
|
182
|
+
const cell = encodeQuadrants([
|
|
183
|
+
pixelAt(leftX, topY),
|
|
184
|
+
pixelAt(rightX, topY),
|
|
185
|
+
pixelAt(leftX, bottomY),
|
|
186
|
+
pixelAt(rightX, bottomY),
|
|
187
|
+
]);
|
|
188
|
+
line += `${foreground(...cell.foregroundColor)}${background(...cell.backgroundColor)}${cell.glyph}`;
|
|
189
|
+
}
|
|
190
|
+
lines.push(`${line}${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderJpegFile(filePath, options = {}) {
|
|
196
|
+
return renderJpegToAnsi(readFileSync(filePath), options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function discoverImages(directory) {
|
|
200
|
+
try {
|
|
201
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
202
|
+
.filter(entry => entry.isFile() && /\.jpe?g$/i.test(entry.name))
|
|
203
|
+
.map(entry => path.join(directory, entry.name))
|
|
204
|
+
.sort((a, b) => a.localeCompare(b));
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createTerminalImageGallery({
|
|
211
|
+
directory = DEFAULT_IMAGE_DIRECTORY,
|
|
212
|
+
frequency = 3,
|
|
213
|
+
env = process.env,
|
|
214
|
+
isTTY = process.stdout.isTTY,
|
|
215
|
+
getColumns = () => process.stdout.columns || 80,
|
|
216
|
+
getRows = () => process.stdout.rows || 24,
|
|
217
|
+
} = {}) {
|
|
218
|
+
const images = discoverImages(directory);
|
|
219
|
+
const enabled = images.length > 0 && supportsTerminalImages({ env, isTTY });
|
|
220
|
+
let imageIndex = 0;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
enabled,
|
|
224
|
+
imageCount: images.length,
|
|
225
|
+
maybeRender(completedQueries = 0, { force = false } = {}) {
|
|
226
|
+
const cadence = Math.max(1, Math.floor(frequency));
|
|
227
|
+
if (!enabled || (!force && (completedQueries <= 0 || completedQueries % cadence !== 0))) return [];
|
|
228
|
+
|
|
229
|
+
const currentIndex = imageIndex % images.length;
|
|
230
|
+
const filePath = images[currentIndex];
|
|
231
|
+
try {
|
|
232
|
+
const imageLines = renderJpegFile(filePath, {
|
|
233
|
+
columns: getColumns(),
|
|
234
|
+
rows: getRows(),
|
|
235
|
+
});
|
|
236
|
+
imageIndex++;
|
|
237
|
+
return [
|
|
238
|
+
'',
|
|
239
|
+
` \x1b[1m\x1b[38;2;255;126;179m◆ KIKU MOMENT\x1b[0m \x1b[38;2;148;163;184m• Gallery ${currentIndex + 1}/${images.length}\x1b[0m`,
|
|
240
|
+
...imageLines,
|
|
241
|
+
'',
|
|
242
|
+
];
|
|
243
|
+
} catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|