kiku-agent 1.0.11 → 1.2.0
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/README.md +300 -495
- package/package.json +12 -11
- package/public/fonts/GoogleSans-Bold.ttf +0 -0
- package/public/fonts/GoogleSans-Bold.woff2 +0 -0
- package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
- package/public/fonts/GoogleSans-Italic.ttf +0 -0
- package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
- package/public/fonts/GoogleSans-Medium.ttf +0 -0
- package/public/fonts/GoogleSans-Medium.woff2 +0 -0
- package/public/fonts/GoogleSans-Regular.ttf +0 -0
- package/public/fonts/GoogleSans-Regular.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
- package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
- package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
- package/public/fonts/google-sans-official.css +900 -0
- package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
- package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/general/jujingyi_portrait.jpg +0 -0
- package/public/images/jujingyi_portrait.jpg +0 -0
- package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
- package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
- package/public/images/series/detective-samoyeds/poster.jpg +0 -0
- package/public/images/series/everyday/poster.jpg +0 -0
- package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
- package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
- package/public/images/series/in-blossom/poster.jpg +0 -0
- package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
- package/public/images/series/lets-fight/poster-2.jpg +0 -0
- package/public/images/series/lets-fight/poster.jpg +0 -0
- package/public/images/series/love-emergency/poster.jpg +0 -0
- package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
- package/public/images/series/money-bastion/poster.jpg +0 -0
- package/public/images/series/mr-swimmer/poster.jpg +0 -0
- package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
- package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
- package/public/images/series/rebirth-for-you/poster.jpg +0 -0
- package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
- package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
- package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
- package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
- package/public/images/series/veil-of-shadows/poster.jpg +0 -0
- package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
- package/public/images/series/wanhua-sin/poster.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
- package/public/manifest.json +25 -0
- package/public/mcp-logo-128.png +0 -0
- package/public/mcp-logo-64.png +0 -0
- package/public/mcp-logo.svg +16 -0
- package/server/agent/agentCore.js +542 -74
- package/server/agent/appPaths.js +1 -0
- package/server/agent/audioPlayer.js +4 -2
- package/server/agent/citations.js +294 -0
- package/server/agent/clarificationAnalytics.js +477 -0
- package/server/agent/clarifyingQuestions.js +1089 -0
- package/server/agent/cli.js +549 -125
- package/server/agent/commandFeatures.js +1 -0
- package/server/agent/commandRegistry.js +946 -879
- package/server/agent/commands.js +546 -272
- package/server/agent/config.js +581 -412
- package/server/agent/contextCompactor.js +2 -2
- package/server/agent/debate.js +235 -0
- package/server/agent/eventBus.js +3 -0
- package/server/agent/feedbackLearning.js +217 -0
- package/server/agent/llmClient.js +512 -394
- package/server/agent/mcpClient.js +182 -32
- package/server/agent/memory.js +283 -224
- package/server/agent/modelManager.js +79 -39
- package/server/agent/profiles.js +178 -0
- package/server/agent/publicTools.js +22 -0
- package/server/agent/queryDetails.js +68 -8
- package/server/agent/quizData.js +398 -384
- package/server/agent/research.js +121 -79
- package/server/agent/sanitizer.js +104 -13
- package/server/agent/session.js +319 -11
- package/server/agent/subagent.js +8 -3
- package/server/agent/suggestionTui.js +294 -0
- package/server/agent/suggestions.js +69 -3
- package/server/agent/telemetry.js +119 -0
- package/server/agent/terminalImage.js +146 -27
- package/server/agent/theme.js +17 -0
- package/server/agent/toolRouter.js +16 -1
- package/server/agent/tools.js +424 -225
- package/server/agent/ui.js +486 -64
- package/server/agent/version.js +2 -4
- package/server/agent/webSearch.js +111 -11
- package/server/agent/youtubeAuth.js +24 -6
- package/server/apiRouter.js +4222 -0
- package/server/mcp/data/discography.js +25 -16
- package/server/mcp/data/gallery.js +308 -20
- package/server/mcp/data/profile.js +74 -44
- package/server/mcp/data/timeline.js +1 -1
- package/server/mcp/data/works.js +51 -17
- package/server/mcp/dataAdapter.js +5 -3
- package/server/mcp/handler.js +5 -11
- package/server/mcp/prompts.js +86 -0
- package/server/mcp/security.js +18 -15
- package/server/mcp/server.js +7 -2
- package/server/mcp/tools.js +66 -2
- package/server/web-chat/core/browserLauncher.js +18 -0
- package/server/web-chat/core/connection.js +194 -0
- package/server/web-chat/core/conversationContext.js +82 -0
- package/server/web-chat/core/directCatalogEngine.js +196 -0
- package/server/web-chat/core/dramaKnowledge.js +142 -0
- package/server/web-chat/core/errorHandler.js +51 -0
- package/server/web-chat/core/followUpEngine.js +182 -0
- package/server/web-chat/core/futureEventGuard.js +80 -0
- package/server/web-chat/core/memorySnapshot.js +12 -0
- package/server/web-chat/core/security.js +146 -0
- package/server/web-chat/core/toolInspector.js +52 -0
- package/server/web-chat/router.js +120 -0
- package/server/web-chat/routes/chatRoute.js +397 -0
- package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
- package/server/web-chat/routes/clarifyRoute.js +83 -0
- package/server/web-chat/routes/compareRoute.js +79 -0
- package/server/web-chat/routes/exportRoute.js +59 -0
- package/server/web-chat/routes/feedbackRoute.js +57 -0
- package/server/web-chat/routes/newsRoute.js +33 -0
- package/server/web-chat/routes/quizRoute.js +11 -0
- package/server/web-chat/routes/staticRoute.js +103 -0
- package/server/web-chat/routes/statusRoute.js +52 -0
- package/server/web-chat/routes/suggestRoute.js +77 -0
- package/server/web-chat/routes/timelineRoute.js +34 -0
- package/server/web-chat/routes/verifyRoute.js +57 -0
- package/server/web-chat/server.js +116 -0
- package/server/web-chat/ui/htmlBuilder.js +91 -0
- package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
- package/server/web-chat/ui/scripts/client.js +8397 -0
- package/server/web-chat/ui/scripts/kikuDb.js +177 -0
- package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
- package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
- package/server/web-chat/ui/styles/main.css +8568 -0
- package/server/web-chat/ui/templates/body.html +796 -0
- package/server/webChatHtml.js +1 -0
- package/server/webChatServer.js +6 -0
- package/src/data/discographyData.js +892 -866
- package/src/data/galleryData.js +1805 -561
- package/src/data/profileData.js +79 -2
- package/src/data/socialUpdatesData.js +106 -47
- package/src/data/timelineData.js +1 -1
- package/src/data/worksData.js +25 -4
- package/tokens.css +97 -0
- package/server/agent/updateManager.js +0 -456
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { C, getCategoryBadge, getStringDisplayWidth, getFullWidth } from './ui.js';
|
|
3
|
+
import { isColorDisabled } from './theme.js';
|
|
4
|
+
|
|
5
|
+
export const SUGGESTION_STUDIO_TABS = Object.freeze([
|
|
6
|
+
'ALL',
|
|
7
|
+
'DRAMA',
|
|
8
|
+
'MUSIC',
|
|
9
|
+
'FASHION',
|
|
10
|
+
'MILESTONE',
|
|
11
|
+
'ANALYSIS',
|
|
12
|
+
'TRIVIA'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function renderSuggestionStudioView({
|
|
16
|
+
suggestions = [],
|
|
17
|
+
selectedIndex = 0,
|
|
18
|
+
selectedIds = new Set(),
|
|
19
|
+
activeCategory = 'ALL',
|
|
20
|
+
termWidth = getFullWidth(),
|
|
21
|
+
} = {}) {
|
|
22
|
+
const width = Math.max(50, Math.min(termWidth - 2, 90));
|
|
23
|
+
const innerWidth = width - 4;
|
|
24
|
+
const rows = [];
|
|
25
|
+
|
|
26
|
+
const filtered = activeCategory === 'ALL'
|
|
27
|
+
? suggestions
|
|
28
|
+
: suggestions.filter(s => String(s.category || '').toUpperCase() === activeCategory);
|
|
29
|
+
|
|
30
|
+
const headerBg = isColorDisabled() ? '' : '\x1b[48;2;65;20;42m';
|
|
31
|
+
const headerFg = isColorDisabled() ? '' : '\x1b[38;2;255;220;235m';
|
|
32
|
+
|
|
33
|
+
rows.push(` ${headerBg}${headerFg}${C.bold} 🌸 KIKU SUGGESTION STUDIO ${C.reset}`);
|
|
34
|
+
rows.push(`${C.dim} Interactive topic explorer • Select multiple with [Space] and launch with [Enter]${C.reset}`);
|
|
35
|
+
rows.push('');
|
|
36
|
+
|
|
37
|
+
const tabTokens = SUGGESTION_STUDIO_TABS.map(tab => {
|
|
38
|
+
const badge = tab === 'ALL' ? { icon: '✨', label: 'ALL', bg: '\x1b[48;2;60;25;40m', color: C.sakura } : getCategoryBadge(tab);
|
|
39
|
+
const isCurrent = tab === activeCategory;
|
|
40
|
+
if (isColorDisabled()) {
|
|
41
|
+
return isCurrent ? `[${badge.icon} ${badge.label}]` : `${badge.icon} ${badge.label}`;
|
|
42
|
+
}
|
|
43
|
+
if (isCurrent) {
|
|
44
|
+
return `\x1b[48;2;212;83;110m\x1b[38;2;255;255;255m\x1b[1m ${badge.icon} ${badge.label} \x1b[0m`;
|
|
45
|
+
}
|
|
46
|
+
return `${badge.bg || '\x1b[48;2;36;32;44m'}\x1b[38;2;180;180;200m ${badge.icon} ${badge.label} \x1b[0m`;
|
|
47
|
+
});
|
|
48
|
+
rows.push(` ${tabTokens.join(' ')}`);
|
|
49
|
+
rows.push('');
|
|
50
|
+
|
|
51
|
+
if (filtered.length === 0) {
|
|
52
|
+
rows.push(` ${C.dim}No suggestions found under ${activeCategory}. Press [Tab] to switch category.${C.reset}`);
|
|
53
|
+
rows.push('');
|
|
54
|
+
} else {
|
|
55
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
56
|
+
const item = filtered[i];
|
|
57
|
+
const isSelected = i === selectedIndex;
|
|
58
|
+
const isChecked = selectedIds.has(item.id);
|
|
59
|
+
const badge = getCategoryBadge(item.category);
|
|
60
|
+
|
|
61
|
+
const pointer = isSelected ? `${C.bold}${C.gold}❯${C.reset}` : ' ';
|
|
62
|
+
const checkMark = isChecked
|
|
63
|
+
? (isColorDisabled() ? '[◉]' : '\x1b[48;2;20;80;40m\x1b[38;2;80;240;140m\x1b[1m[◉]\x1b[0m')
|
|
64
|
+
: (isColorDisabled() ? '[ ]' : '\x1b[48;2;35;35;45m\x1b[38;2;160;160;180m[ ]\x1b[0m');
|
|
65
|
+
const numLabel = `${item.id}.`;
|
|
66
|
+
const catBadge = isColorDisabled()
|
|
67
|
+
? `[${badge.icon} ${badge.label}]`
|
|
68
|
+
: `${badge.bg}${badge.color}${C.bold} ${badge.icon} ${badge.label} ${C.reset}`;
|
|
69
|
+
|
|
70
|
+
let qText = item.question;
|
|
71
|
+
const availableTextWidth = Math.max(10, innerWidth - 20);
|
|
72
|
+
if (getStringDisplayWidth(qText) > availableTextWidth) {
|
|
73
|
+
let truncated = '';
|
|
74
|
+
for (const char of qText) {
|
|
75
|
+
if (getStringDisplayWidth(truncated + char) > availableTextWidth - 2) break;
|
|
76
|
+
truncated += char;
|
|
77
|
+
}
|
|
78
|
+
qText = `${truncated}…`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const qStyled = isSelected ? `${C.bold}${C.white}${qText}${C.reset}` : `${C.white}${qText}${C.reset}`;
|
|
82
|
+
rows.push(` ${pointer} ${checkMark} ${C.bold}${numLabel}${C.reset} ${catBadge} ${qStyled}`);
|
|
83
|
+
|
|
84
|
+
if (isSelected) {
|
|
85
|
+
const descText = item.description || 'Explore deep multi-agent insights and citations.';
|
|
86
|
+
const cleanDesc = descText.length > innerWidth - 6 ? `${descText.slice(0, innerWidth - 9)}…` : descText;
|
|
87
|
+
const focusTag = isColorDisabled() ? 'Focus:' : '\x1b[48;2;45;20;32m\x1b[38;2;255;190;210m Focus: \x1b[0m';
|
|
88
|
+
rows.push(` ${focusTag} ${C.slate}${cleanDesc}${C.reset}`);
|
|
89
|
+
|
|
90
|
+
const toolsStr = Array.isArray(item.tools) ? item.tools.join(', ') : 'search_works';
|
|
91
|
+
const depthStr = item.depth || 'Deep Analysis';
|
|
92
|
+
const estStr = `~${item.estimatedSeconds || 2}s`;
|
|
93
|
+
|
|
94
|
+
const toolTag = isColorDisabled() ? 'Tools:' : '\x1b[48;2;20;40;55m\x1b[38;2;120;220;255m Tools: \x1b[0m';
|
|
95
|
+
const estTag = isColorDisabled() ? 'Est:' : '\x1b[48;2;45;38;20m\x1b[38;2;255;210;80m Est: \x1b[0m';
|
|
96
|
+
const depthTag = isColorDisabled() ? 'Depth:' : '\x1b[48;2;20;50;35m\x1b[38;2;100;240;150m Depth: \x1b[0m';
|
|
97
|
+
|
|
98
|
+
rows.push(` ${toolTag} ${C.slate}${toolsStr}${C.reset} ${estTag} ${C.slate}${estStr}${C.reset} ${depthTag} ${C.slate}${depthStr}${C.reset}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
rows.push('');
|
|
104
|
+
const checkedCount = selectedIds.size;
|
|
105
|
+
const launchLabel = checkedCount > 0 ? `Launch Batch (${checkedCount})` : 'Launch (1)';
|
|
106
|
+
const navBar = isColorDisabled()
|
|
107
|
+
? `[↑/↓] Nav • [Space] Select • [Enter] ${launchLabel} • [Tab] Tab • [A] All • [Esc] Cancel`
|
|
108
|
+
: `\x1b[48;2;36;28;44m\x1b[38;2;225;210;245m [↑/↓] Nav • [Space] Select • [Enter] ${launchLabel} • [Tab] Tab • [A] All • [Esc] Cancel \x1b[0m`;
|
|
109
|
+
rows.push(` ${navBar}`);
|
|
110
|
+
rows.push(` ${C.dim}[1-9] Quick Toggle • [R] Refresh • [Q] Exit${C.reset}`);
|
|
111
|
+
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
export async function promptInteractiveSuggestion(suggestions = [], options = {}) {
|
|
117
|
+
const stdin = options.stdin || process.stdin;
|
|
118
|
+
const stdout = options.stdout || process.stdout;
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if (!stdin.isTTY || !stdout.isTTY || options.plain || process.env.KIKU_PLAIN === 'true') {
|
|
122
|
+
return { action: 'display', targets: null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let selectedIndex = 0;
|
|
127
|
+
const selectedIds = new Set();
|
|
128
|
+
let currentTabIdx = 0;
|
|
129
|
+
let activeCategory = SUGGESTION_STUDIO_TABS[0];
|
|
130
|
+
let isCleanedUp = false;
|
|
131
|
+
let renderedRowCount = 0;
|
|
132
|
+
|
|
133
|
+
const getFiltered = () => {
|
|
134
|
+
return activeCategory === 'ALL'
|
|
135
|
+
? suggestions
|
|
136
|
+
: suggestions.filter(s => String(s.category || '').toUpperCase() === activeCategory);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
if (isCleanedUp) return;
|
|
141
|
+
isCleanedUp = true;
|
|
142
|
+
stdin.removeListener('keypress', handleKey);
|
|
143
|
+
try { stdin.setRawMode(false); } catch {}
|
|
144
|
+
|
|
145
|
+
if (renderedRowCount > 0) {
|
|
146
|
+
readline.cursorTo(stdout, 0);
|
|
147
|
+
readline.moveCursor(stdout, 0, -renderedRowCount + 1);
|
|
148
|
+
readline.clearScreenDown(stdout);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const render = () => {
|
|
153
|
+
if (isCleanedUp) return;
|
|
154
|
+
const termWidth = stdout.columns || 80;
|
|
155
|
+
const frameRows = renderSuggestionStudioView({
|
|
156
|
+
suggestions,
|
|
157
|
+
selectedIndex,
|
|
158
|
+
selectedIds,
|
|
159
|
+
activeCategory,
|
|
160
|
+
termWidth,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (renderedRowCount > 0) {
|
|
164
|
+
readline.cursorTo(stdout, 0);
|
|
165
|
+
readline.moveCursor(stdout, 0, -renderedRowCount + 1);
|
|
166
|
+
readline.clearScreenDown(stdout);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
stdout.write(frameRows.join('\n') + '\n');
|
|
170
|
+
renderedRowCount = frameRows.length;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleKey = (str, key) => {
|
|
174
|
+
if (isCleanedUp) return;
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if ((key?.ctrl && key?.name === 'c') || key?.name === 'escape' || str === 'q' || str === 'Q') {
|
|
178
|
+
cleanup();
|
|
179
|
+
return resolve({ action: 'cancel' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const filtered = getFiltered();
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if (key?.name === 'up' || str === 'k') {
|
|
186
|
+
if (filtered.length > 0) {
|
|
187
|
+
selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length;
|
|
188
|
+
render();
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if (key?.name === 'down' || str === 'j') {
|
|
195
|
+
if (filtered.length > 0) {
|
|
196
|
+
selectedIndex = (selectedIndex + 1) % filtered.length;
|
|
197
|
+
render();
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if (key?.name === 'tab' || key?.name === 'right') {
|
|
204
|
+
currentTabIdx = (currentTabIdx + 1) % SUGGESTION_STUDIO_TABS.length;
|
|
205
|
+
activeCategory = SUGGESTION_STUDIO_TABS[currentTabIdx];
|
|
206
|
+
selectedIndex = 0;
|
|
207
|
+
render();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key?.name === 'left') {
|
|
211
|
+
currentTabIdx = (currentTabIdx - 1 + SUGGESTION_STUDIO_TABS.length) % SUGGESTION_STUDIO_TABS.length;
|
|
212
|
+
activeCategory = SUGGESTION_STUDIO_TABS[currentTabIdx];
|
|
213
|
+
selectedIndex = 0;
|
|
214
|
+
render();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if (key?.name === 'space' || str === ' ') {
|
|
220
|
+
const item = filtered[selectedIndex];
|
|
221
|
+
if (item) {
|
|
222
|
+
if (selectedIds.has(item.id)) {
|
|
223
|
+
selectedIds.delete(item.id);
|
|
224
|
+
} else {
|
|
225
|
+
selectedIds.add(item.id);
|
|
226
|
+
}
|
|
227
|
+
render();
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if (str === 'a' || str === 'A') {
|
|
234
|
+
const allChecked = filtered.every(s => selectedIds.has(s.id));
|
|
235
|
+
if (allChecked) {
|
|
236
|
+
filtered.forEach(s => selectedIds.delete(s.id));
|
|
237
|
+
} else {
|
|
238
|
+
filtered.forEach(s => selectedIds.add(s.id));
|
|
239
|
+
}
|
|
240
|
+
render();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if (str === 'r' || str === 'R') {
|
|
246
|
+
cleanup();
|
|
247
|
+
return resolve({ action: 'refresh' });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if (/^[1-9]$/.test(str)) {
|
|
252
|
+
const num = Number.parseInt(str, 10);
|
|
253
|
+
const matchIdx = filtered.findIndex(s => s.id === num);
|
|
254
|
+
if (matchIdx >= 0) {
|
|
255
|
+
selectedIndex = matchIdx;
|
|
256
|
+
if (selectedIds.has(num)) {
|
|
257
|
+
selectedIds.delete(num);
|
|
258
|
+
} else {
|
|
259
|
+
selectedIds.add(num);
|
|
260
|
+
}
|
|
261
|
+
render();
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if (key?.name === 'return' || str === '\r' || str === '\n') {
|
|
268
|
+
cleanup();
|
|
269
|
+
let targetStr = '';
|
|
270
|
+
if (selectedIds.size > 0) {
|
|
271
|
+
targetStr = Array.from(selectedIds).sort((a, b) => a - b).join(',');
|
|
272
|
+
} else {
|
|
273
|
+
const item = filtered[selectedIndex] || suggestions[0];
|
|
274
|
+
targetStr = item ? String(item.id) : '1';
|
|
275
|
+
}
|
|
276
|
+
return resolve({ action: 'run', targets: targetStr });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
readline.emitKeypressEvents(stdin);
|
|
282
|
+
try { stdin.setRawMode(true); } catch {}
|
|
283
|
+
stdin.on('keypress', handleKey);
|
|
284
|
+
|
|
285
|
+
if (options.signal) {
|
|
286
|
+
options.signal.addEventListener('abort', () => {
|
|
287
|
+
cleanup();
|
|
288
|
+
resolve({ action: 'cancel' });
|
|
289
|
+
}, { once: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
render();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
@@ -16,44 +16,89 @@ export const CURATED_SUGGESTION_TEMPLATES = Object.freeze([
|
|
|
16
16
|
category: 'DRAMA',
|
|
17
17
|
question: "What are Ju Jingyi's upcoming 2026/2025 drama projects, characters, and release platforms?",
|
|
18
18
|
description: "Detailed breakdown of upcoming dramas, character names, and broadcasting platforms.",
|
|
19
|
+
tools: ['search_works', 'web_search'],
|
|
20
|
+
depth: 'Deep Analysis',
|
|
21
|
+
estimatedSeconds: 3,
|
|
19
22
|
},
|
|
20
23
|
{
|
|
21
24
|
category: 'MUSIC',
|
|
22
25
|
question: "What are the most popular OSTs and solo singles released by Ju Jingyi, and what are their YouTube links?",
|
|
23
26
|
description: "Discography highlights from In Blossom, Ruyi Pavilion, and Legend of Yunxi with video links.",
|
|
27
|
+
tools: ['search_discography', 'search_videos'],
|
|
28
|
+
depth: 'Media Scan',
|
|
29
|
+
estimatedSeconds: 2,
|
|
24
30
|
},
|
|
25
31
|
{
|
|
26
32
|
category: 'FASHION',
|
|
27
33
|
question: "What are Ju Jingyi's major luxury brand ambassadorships and global fashion partnerships in 2025-2026?",
|
|
28
34
|
description: "Overview of Versace Global Brand Ambassador role, Maybelline, and top endorsements.",
|
|
35
|
+
tools: ['get_profile_and_fandom', 'search_works'],
|
|
36
|
+
depth: 'Portfolio Scan',
|
|
37
|
+
estimatedSeconds: 2,
|
|
29
38
|
},
|
|
30
39
|
{
|
|
31
40
|
category: 'MILESTONE',
|
|
32
41
|
question: "How did Ju Jingyi achieve back-to-back #1 victories in the SNH48 General Elections and transition to her solo studio?",
|
|
33
42
|
description: "Chronological analysis of the SNH48 Hall of Fame elevation and independent studio milestones.",
|
|
43
|
+
tools: ['get_career_timeline', 'search_awards'],
|
|
44
|
+
depth: 'Historical Deep Dive',
|
|
45
|
+
estimatedSeconds: 3,
|
|
34
46
|
},
|
|
35
47
|
{
|
|
36
48
|
category: 'ANALYSIS',
|
|
37
49
|
question: "What acting awards and cultural influence honors has Ju Jingyi received across Chinese television and media?",
|
|
38
50
|
description: "Awards summary spanning Weibo Night, Baidu Feast, Tencent Starlight, and drama recognitions.",
|
|
51
|
+
tools: ['search_awards', 'get_career_timeline'],
|
|
52
|
+
depth: 'Accolades Breakdown',
|
|
53
|
+
estimatedSeconds: 2,
|
|
39
54
|
},
|
|
40
55
|
{
|
|
41
56
|
category: 'TRIVIA',
|
|
42
57
|
question: "What is the meaning and background of Ju Jingyi's fan community name 'Mìjú' (蜜橘) and official fandom colors?",
|
|
43
58
|
description: "Biographical details, fan community history, official colors, and community traditions.",
|
|
59
|
+
tools: ['get_profile_and_fandom'],
|
|
60
|
+
depth: 'Fandom Trivia',
|
|
61
|
+
estimatedSeconds: 1,
|
|
44
62
|
},
|
|
45
63
|
{
|
|
46
64
|
category: 'DRAMA',
|
|
47
65
|
question: "How did Ju Jingyi's performance in 'In Blossom' (花间令) as Yang Caiwei receive audience acclaim and streaming milestones?",
|
|
48
66
|
description: "Deep dive into character duality, critical reception, and viewership records.",
|
|
67
|
+
tools: ['search_works', 'search_awards'],
|
|
68
|
+
depth: 'Performance Review',
|
|
69
|
+
estimatedSeconds: 2,
|
|
49
70
|
},
|
|
50
71
|
{
|
|
51
72
|
category: 'MUSIC',
|
|
52
73
|
question: "What are Ju Jingyi's live stage performances and music videos with the highest viewership?",
|
|
53
74
|
description: "Top viral stage performances, traditional guofeng choreography, and music video releases.",
|
|
75
|
+
tools: ['search_discography', 'search_videos'],
|
|
76
|
+
depth: 'Visual Stages',
|
|
77
|
+
estimatedSeconds: 2,
|
|
54
78
|
},
|
|
55
79
|
]);
|
|
56
80
|
|
|
81
|
+
export async function mapConcurrent(items, limit, fn) {
|
|
82
|
+
const results = new Array(items.length);
|
|
83
|
+
const maxLimit = Math.max(1, limit || 1);
|
|
84
|
+
let nextIndex = 0;
|
|
85
|
+
|
|
86
|
+
const workers = Array.from({ length: Math.min(maxLimit, items.length) }, async () => {
|
|
87
|
+
while (nextIndex < items.length) {
|
|
88
|
+
const currentIndex = nextIndex++;
|
|
89
|
+
try {
|
|
90
|
+
const val = await fn(items[currentIndex], currentIndex);
|
|
91
|
+
results[currentIndex] = { status: 'fulfilled', value: val };
|
|
92
|
+
} catch (reason) {
|
|
93
|
+
results[currentIndex] = { status: 'rejected', reason };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await Promise.all(workers);
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
|
|
57
102
|
export class SuggestionEngine {
|
|
58
103
|
constructor(options = {}) {
|
|
59
104
|
this.client = options.client || null;
|
|
@@ -89,6 +134,9 @@ export class SuggestionEngine {
|
|
|
89
134
|
category: item.category,
|
|
90
135
|
question: item.question,
|
|
91
136
|
description: item.description,
|
|
137
|
+
tools: item.tools || ['search_works'],
|
|
138
|
+
depth: item.depth || 'Standard',
|
|
139
|
+
estimatedSeconds: item.estimatedSeconds || 2,
|
|
92
140
|
source: 'catalog',
|
|
93
141
|
}));
|
|
94
142
|
|
|
@@ -98,6 +146,11 @@ export class SuggestionEngine {
|
|
|
98
146
|
async generateSuggestions(options = {}) {
|
|
99
147
|
const limit = Math.min(Math.max(options.limit || 5, 2), 8);
|
|
100
148
|
const topic = (options.topic || '').trim();
|
|
149
|
+
const lang = String(options.lang || 'en').toLowerCase();
|
|
150
|
+
const langNames = { th: 'Thai', zh: 'Simplified Chinese', en: 'English', ja: 'Japanese', ko: 'Korean' };
|
|
151
|
+
const langInstruction = langNames[lang]
|
|
152
|
+
? `Write every question and description entirely in ${langNames[lang]} (no English except proper nouns like drama titles).`
|
|
153
|
+
: '';
|
|
101
154
|
|
|
102
155
|
if (!this.client || typeof this.client.chat !== 'function') {
|
|
103
156
|
return this.generateCatalogSuggestions(options);
|
|
@@ -111,6 +164,7 @@ export class SuggestionEngine {
|
|
|
111
164
|
const systemPrompt = `You are the Ju Jingyi intelligence coordinator for Kiku Agent.
|
|
112
165
|
Generate ${limit} distinct, highly specific, and exciting research questions about Ju Jingyi that fans or researchers would want to explore.
|
|
113
166
|
${topicInstruction}
|
|
167
|
+
${langInstruction}
|
|
114
168
|
|
|
115
169
|
Output ONLY a valid JSON array of objects with the exact schema:
|
|
116
170
|
[
|
|
@@ -123,7 +177,7 @@ Output ONLY a valid JSON array of objects with the exact schema:
|
|
|
123
177
|
Do not include markdown code fences or any text other than the JSON array.`;
|
|
124
178
|
|
|
125
179
|
const controller = new AbortController();
|
|
126
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
180
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
127
181
|
if (options.signal) {
|
|
128
182
|
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
129
183
|
}
|
|
@@ -153,6 +207,9 @@ Do not include markdown code fences or any text other than the JSON array.`;
|
|
|
153
207
|
category: String(item.category || 'ANALYSIS').toUpperCase(),
|
|
154
208
|
question: String(item.question || '').trim(),
|
|
155
209
|
description: String(item.description || '').trim(),
|
|
210
|
+
tools: Array.isArray(item.tools) && item.tools.length > 0 ? item.tools : ['search_works'],
|
|
211
|
+
depth: item.depth || 'Deep Analysis',
|
|
212
|
+
estimatedSeconds: item.estimatedSeconds || 3,
|
|
156
213
|
source: 'ai_model',
|
|
157
214
|
})).filter(s => s.question.length > 5);
|
|
158
215
|
|
|
@@ -236,8 +293,17 @@ Do not include markdown code fences or any text other than the JSON array.`;
|
|
|
236
293
|
})),
|
|
237
294
|
});
|
|
238
295
|
|
|
296
|
+
const isLocalProvider = Boolean(
|
|
297
|
+
this.client?.provider === 'lmstudio' ||
|
|
298
|
+
this.client?.provider === 'ollama' ||
|
|
299
|
+
this.client?.isLocal ||
|
|
300
|
+
this.client?.getProviderInfo?.()?.isLocal
|
|
301
|
+
);
|
|
302
|
+
const defaultLimit = isLocalProvider ? 1 : this.maxWorkers;
|
|
303
|
+
const concurrency = Math.min(Math.max(options.concurrency || defaultLimit, 1), this.maxWorkers || 5);
|
|
304
|
+
|
|
239
305
|
try {
|
|
240
|
-
const settled = await
|
|
306
|
+
const settled = await mapConcurrent(tasks, concurrency, async (task, index) => {
|
|
241
307
|
const workerId = `worker_${index + 1}`;
|
|
242
308
|
const workerLabel = `[Worker ${index + 1}] ${task.category || 'Research'}`;
|
|
243
309
|
|
|
@@ -298,7 +364,7 @@ Do not include markdown code fences or any text other than the JSON array.`;
|
|
|
298
364
|
toolCalls: result.toolCalls,
|
|
299
365
|
iterations: result.iterations,
|
|
300
366
|
};
|
|
301
|
-
})
|
|
367
|
+
});
|
|
302
368
|
|
|
303
369
|
const results = settled.map((entry, index) => {
|
|
304
370
|
if (entry.status === 'fulfilled') return entry.value;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export const MODEL_PRICING = {
|
|
2
|
+
lmstudio: {
|
|
3
|
+
default: { in: 0.0, out: 0.0 },
|
|
4
|
+
'google/gemma-4-e4b': { in: 0.0, out: 0.0 },
|
|
5
|
+
'google/gemma-4-e4b-Q4_K_M': { in: 0.0, out: 0.0 },
|
|
6
|
+
'google/gemma-4-e4b-Q6_K': { in: 0.0, out: 0.0 },
|
|
7
|
+
'google/gemma-4-e4b-Q8_0': { in: 0.0, out: 0.0 }
|
|
8
|
+
},
|
|
9
|
+
groq: {
|
|
10
|
+
default: { in: 0.15, out: 0.60 },
|
|
11
|
+
'openai/gpt-oss-120b': { in: 0.15, out: 0.60 },
|
|
12
|
+
'openai/gpt-oss-20b': { in: 0.075, out: 0.30 }
|
|
13
|
+
},
|
|
14
|
+
deepseek: {
|
|
15
|
+
default: { in: 0.30, out: 1.20 },
|
|
16
|
+
'deepseek-v4-flash': { in: 0.27, out: 1.10 },
|
|
17
|
+
'deepseek-v4-pro': { in: 0.99, out: 2.19 }
|
|
18
|
+
},
|
|
19
|
+
openai: {
|
|
20
|
+
default: { in: 0.20, out: 1.20 },
|
|
21
|
+
'gpt-5.6-luna': { in: 0.20, out: 1.20 },
|
|
22
|
+
'gpt-5.6-terra': { in: 2.00, out: 6.00 },
|
|
23
|
+
'gpt-5.6-sol': { in: 4.00, out: 12.00 }
|
|
24
|
+
},
|
|
25
|
+
gemini: {
|
|
26
|
+
default: { in: 0.75, out: 3.00 },
|
|
27
|
+
'gemini-3.7-flash': { in: 0.75, out: 3.00 },
|
|
28
|
+
'gemini-3.5-flash-lite': { in: 0.30, out: 1.20 },
|
|
29
|
+
'gemini-3.6-flash': { in: 0.75, out: 3.00 }
|
|
30
|
+
},
|
|
31
|
+
openrouter: {
|
|
32
|
+
default: { in: 0.20, out: 0.80 },
|
|
33
|
+
'openrouter/auto': { in: 0.20, out: 0.80 }
|
|
34
|
+
},
|
|
35
|
+
custom: {
|
|
36
|
+
default: { in: 0.0, out: 0.0 }
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function estimateTokens(text) {
|
|
42
|
+
if (!text || typeof text !== 'string') return 0;
|
|
43
|
+
const str = text.trim();
|
|
44
|
+
if (!str) return 0;
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const cjkAndThai = str.match(/[\u4e00-\u9fa5\u0e00-\u0e7f\u3040-\u30ff\uac00-\ud7af]/g) || [];
|
|
48
|
+
const nonCjk = str.replace(/[\u4e00-\u9fa5\u0e00-\u0e7f\u3040-\u30ff\uac00-\ud7af]/g, ' ');
|
|
49
|
+
const words = nonCjk.split(/\s+/).filter(Boolean);
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
const cjkTokens = Math.ceil(cjkAndThai.length * 1.25);
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const wordTokens = Math.ceil(words.length * 1.3);
|
|
56
|
+
|
|
57
|
+
return Math.max(1, cjkTokens + wordTokens);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export function calculateTokenCost(providerOrTokens, modelOrProvider, promptTokensOrModel = 0, completionTokens = 0) {
|
|
62
|
+
let pId, mId, prompt, completion;
|
|
63
|
+
if (typeof providerOrTokens === 'object' && providerOrTokens !== null) {
|
|
64
|
+
prompt = Number(providerOrTokens.prompt || 0);
|
|
65
|
+
completion = Number(providerOrTokens.completion || 0);
|
|
66
|
+
pId = String(modelOrProvider || 'lmstudio').toLowerCase().trim();
|
|
67
|
+
mId = String(promptTokensOrModel || 'default').trim();
|
|
68
|
+
} else {
|
|
69
|
+
pId = String(providerOrTokens || 'lmstudio').toLowerCase().trim();
|
|
70
|
+
mId = String(modelOrProvider || 'default').trim();
|
|
71
|
+
prompt = Number(promptTokensOrModel || 0);
|
|
72
|
+
completion = Number(completionTokens || 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const providerPricing = MODEL_PRICING[pId] || MODEL_PRICING.custom;
|
|
76
|
+
const rates = providerPricing[mId] || providerPricing.default || { in: 0, out: 0 };
|
|
77
|
+
|
|
78
|
+
const inputCost = (prompt * rates.in) / 1_000_000;
|
|
79
|
+
const outputCost = (completion * rates.out) / 1_000_000;
|
|
80
|
+
|
|
81
|
+
return Number((inputCost + outputCost).toFixed(6));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Human-readable formatted cost in USD
|
|
86
|
+
*/
|
|
87
|
+
export function formatCostUsd(amount, isLocal = false) {
|
|
88
|
+
const num = Number(amount || 0);
|
|
89
|
+
if (isLocal || num === 0) {
|
|
90
|
+
return '$0.00 (Local)';
|
|
91
|
+
}
|
|
92
|
+
if (num < 0.0001) {
|
|
93
|
+
return '<$0.0001';
|
|
94
|
+
}
|
|
95
|
+
return `$${num.toFixed(4)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format token count with thousand separators
|
|
100
|
+
*/
|
|
101
|
+
export function formatTokenCount(count) {
|
|
102
|
+
const num = Number(count || 0);
|
|
103
|
+
return num.toLocaleString('en-US');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a normalized token usage object
|
|
108
|
+
*/
|
|
109
|
+
export function createTokenUsage({ prompt = 0, completion = 0, reasoning = 0 } = {}) {
|
|
110
|
+
const p = Math.max(0, Math.round(Number(prompt) || 0));
|
|
111
|
+
const c = Math.max(0, Math.round(Number(completion) || 0));
|
|
112
|
+
const r = Math.max(0, Math.round(Number(reasoning) || 0));
|
|
113
|
+
return {
|
|
114
|
+
prompt: p,
|
|
115
|
+
completion: c,
|
|
116
|
+
reasoning: r,
|
|
117
|
+
total: p + c
|
|
118
|
+
};
|
|
119
|
+
}
|