Skip to content

Commit c78c76e

Browse files
committed
Add support for the Kimi chatbot integration
1 parent 421a10c commit c78c76e

8 files changed

Lines changed: 209 additions & 4 deletions

File tree

packages/browser/src/content-scripts/send-prompt-content-script/chatbots/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './qwen'
1111
export * from './yuanbao'
1212
export * from './doubao'
1313
export * from './huggingchat'
14+
export * from './kimi'
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { Chatbot } from '../types/chatbot'
2+
import browser from 'webextension-polyfill'
3+
import {
4+
apply_chat_response_button_style,
5+
set_button_disabled_state
6+
} from '../utils/apply-response-styles'
7+
import { Message } from '@/types/messages'
8+
import { is_eligible_code_block } from '../utils/is-eligible-code-block'
9+
import {
10+
apply_response_button_text,
11+
apply_response_button_title
12+
} from '../constants/copy'
13+
import { show_response_ready_notification } from '../utils/show-response-ready-notification'
14+
15+
export const kimi: Chatbot = {
16+
wait_until_ready: async () => {
17+
await new Promise((resolve) => {
18+
const check_for_element = () => {
19+
if (document.querySelector('.home-case-list')) {
20+
resolve(null)
21+
} else {
22+
setTimeout(check_for_element, 100)
23+
}
24+
}
25+
check_for_element()
26+
})
27+
},
28+
enter_message_and_send: async (message: string) => {
29+
const input_element = document.querySelector(
30+
'.chat-input-editor[data-lexical-editor="true"]'
31+
) as HTMLElement
32+
33+
if (!input_element) {
34+
throw new Error('Kimi input element not found')
35+
}
36+
37+
// Focus the element first
38+
input_element.focus()
39+
40+
// Clear existing content by selecting all and deleting
41+
document.execCommand('selectAll', false)
42+
document.execCommand('delete', false)
43+
44+
// Insert the new text using execCommand which works better with Lexical
45+
document.execCommand('insertText', false, message)
46+
47+
// Dispatch input event to trigger Lexical's change detection
48+
input_element.dispatchEvent(new Event('input', { bubbles: true }))
49+
input_element.dispatchEvent(new Event('change', { bubbles: true }))
50+
51+
new Promise<void>((resolve) => {
52+
const check_button_state = () => {
53+
const send_button_container = document.querySelector(
54+
'.send-button-container'
55+
)
56+
const send_button = document.querySelector(
57+
'div.send-button'
58+
) as HTMLElement
59+
60+
if (
61+
send_button &&
62+
!send_button_container?.classList.contains('disabled')
63+
) {
64+
send_button.click()
65+
resolve()
66+
} else {
67+
setTimeout(check_button_state, 100)
68+
}
69+
}
70+
check_button_state()
71+
})
72+
},
73+
inject_apply_response_button: (client_id: number) => {
74+
const add_buttons = (params: { footer: Element }) => {
75+
// Check if buttons already exist by text content to avoid duplicates
76+
const existing_apply_response_button = Array.from(
77+
params.footer.querySelectorAll('button')
78+
).find((btn) => btn.textContent == apply_response_button_text)
79+
80+
if (existing_apply_response_button) return
81+
82+
const chat_turn = params.footer.closest('.segment-content') as HTMLElement
83+
const code_blocks = chat_turn.querySelectorAll('code')
84+
let has_eligible_block = false
85+
for (const code_block of Array.from(code_blocks)) {
86+
const first_line_text = code_block?.textContent?.split('\n')[0]
87+
if (first_line_text && is_eligible_code_block(first_line_text)) {
88+
has_eligible_block = true
89+
break
90+
}
91+
}
92+
if (!has_eligible_block) return
93+
94+
const create_apply_response_button = () => {
95+
const apply_response_button = document.createElement('button')
96+
apply_response_button.textContent = apply_response_button_text
97+
apply_response_button.title = apply_response_button_title
98+
apply_chat_response_button_style(apply_response_button)
99+
100+
apply_response_button.addEventListener('click', async () => {
101+
set_button_disabled_state(apply_response_button)
102+
const copy_button = document.querySelector(
103+
'.segment-assistant-actions-content > div:first-child'
104+
) as HTMLElement
105+
copy_button.click()
106+
await new Promise((resolve) => setTimeout(resolve, 500))
107+
browser.runtime.sendMessage<Message>({
108+
action: 'apply-chat-response',
109+
client_id
110+
})
111+
})
112+
113+
params.footer.insertBefore(
114+
apply_response_button,
115+
params.footer.children[6]
116+
)
117+
118+
apply_response_button.focus()
119+
}
120+
121+
create_apply_response_button()
122+
}
123+
124+
const observer = new MutationObserver((mutations) => {
125+
mutations.forEach(() => {
126+
if (
127+
document.querySelector(
128+
'path[d="M331.946667 379.904c-11.946667 23.466667-11.946667 54.186667-11.946667 115.626667v32.938666c0 61.44 0 92.16 11.946667 115.626667 10.538667 20.650667 27.306667 37.418667 47.957333 47.957333 23.466667 11.946667 54.186667 11.946667 115.626667 11.946667h32.938666c61.44 0 92.16 0 115.626667-11.946667 20.650667-10.538667 37.418667-27.306667 47.957333-47.957333 11.946667-23.466667 11.946667-54.186667 11.946667-115.626667v-32.938666c0-61.44 0-92.16-11.946667-115.626667a109.696 109.696 0 0 0-47.957333-47.957333c-23.466667-11.946667-54.186667-11.946667-115.626667-11.946667h-32.938666c-61.44 0-92.16 0-115.626667 11.946667-20.650667 10.538667-37.418667 27.306667-47.957333 47.957333z"]'
129+
)
130+
) {
131+
return
132+
}
133+
134+
show_response_ready_notification({ chatbot_name: 'Kimi' })
135+
136+
const all_footers = document.querySelectorAll(
137+
'.segment-assistant-actions-content'
138+
)
139+
all_footers.forEach((footer) => {
140+
add_buttons({
141+
footer
142+
})
143+
})
144+
})
145+
})
146+
147+
observer.observe(document.body, {
148+
childList: true,
149+
subtree: true,
150+
characterData: true
151+
})
152+
}
153+
}

packages/browser/src/content-scripts/send-prompt-content-script/send-prompt-content-script.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
qwen,
1616
yuanbao,
1717
doubao,
18-
huggingchat
18+
huggingchat,
19+
kimi
1920
} from './chatbots'
2021
import { perplexity } from './chatbots/perplexity'
2122

@@ -70,7 +71,9 @@ const is_doubao = current_url.startsWith(doubao_url)
7071
const huggingchat_url = 'https://huggingface.co/chat/'
7172
const is_huggingchat = current_url.startsWith(huggingchat_url)
7273

73-
// perplexity
74+
const kimi_url = 'https://www.kimi.com/'
75+
const is_kimi = current_url.startsWith(kimi_url)
76+
7477
const perplexity_url = 'https://www.perplexity.ai/'
7578
const is_perplexity = current_url.startsWith(perplexity_url)
7679

@@ -106,6 +109,8 @@ if (is_ai_studio) {
106109
chatbot = huggingchat
107110
} else if (is_perplexity) {
108111
chatbot = perplexity
112+
} else if (is_kimi) {
113+
chatbot = kimi
109114
}
110115

111116
export const get_textarea_element = () => {
@@ -118,6 +123,7 @@ export const get_textarea_element = () => {
118123
[deepseek_url]: 'textarea',
119124
[mistral_url]: 'div[contenteditable="true"]',
120125
[yuanbao_url]: 'div[contenteditable="true"]',
126+
[kimi_url]: 'div[contenteditable="true"]',
121127
[doubao_url]: 'textarea',
122128
[perplexity_url]: 'textarea'
123129
} as any
@@ -173,14 +179,14 @@ const enter_message_and_send = async (params: {
173179
params.input_element &&
174180
params.input_element.tagName == 'TEXTAREA'
175181
) {
176-
const form = params.input_element.closest('form')
177182
;(params.input_element as HTMLTextAreaElement).value = params.message
178183
params.input_element.dispatchEvent(new Event('input', { bubbles: true }))
179184
params.input_element.dispatchEvent(new Event('change', { bubbles: true }))
180185
await new Promise((r) => requestAnimationFrame(r))
181186
if (is_openrouter || is_mistral) {
182187
await new Promise((r) => setTimeout(r, 500))
183188
}
189+
const form = params.input_element.closest('form')
184190
if (form) {
185191
form.requestSubmit()
186192
} else if (is_ai_studio) {

packages/browser/src/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"author": "Robert Piosik",
44
"name": "Code Web Chat Connector",
55
"short_name": "CWC",
6-
"description": "Initializes chatbots and lets you manage websites for context.",
6+
"description": "Initialize chatbots and manage websites for context.",
77
"version": "1.7.0",
88
"homepage_url": "https://github.com/robertpiosik/CodeWebChat",
99
"icons": {
@@ -37,6 +37,7 @@
3737
"https://yuanbao.tencent.com/chat*",
3838
"https://www.doubao.com/chat/",
3939
"https://www.perplexity.ai/",
40+
"https://www.kimi.com/",
4041
"http://localhost/",
4142
"http://openwebui/"
4243
],

packages/shared/src/constants/chatbots.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,19 @@ export const CHATBOTS = {
218218
default_top_p: 0,
219219
models: {}
220220
},
221+
Kimi: {
222+
url: 'https://www.kimi.com/',
223+
supports_custom_temperature: false,
224+
supports_custom_top_p: false,
225+
supports_system_instructions: false,
226+
supports_user_provided_model: false,
227+
supports_user_provided_port: false,
228+
supports_thinking_budget: false,
229+
default_system_instructions: '',
230+
supported_options: {},
231+
default_top_p: 0,
232+
models: {}
233+
},
221234
Perplexity: {
222235
url: 'https://www.perplexity.ai/',
223236
supports_custom_temperature: false,
Lines changed: 25 additions & 0 deletions
Loading

packages/ui/src/components/editor/Icon/Icon.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import DeepSeek from '../../../assets/icons/deepseek.svg'
99
import Mistral from '../../../assets/icons/mistral.svg'
1010
import Grok from '../../../assets/icons/grok.svg'
1111
import HuggingChat from '../../../assets/icons/hugging-chat.svg'
12+
import Kimi from '../../../assets/icons/kimi.svg'
1213
import Qwen from '../../../assets/icons/qwen.svg'
1314
import Yuanbao from '../../../assets/icons/yuanbao.svg'
1415
import Doubao from '../../../assets/icons/doubao.svg'
@@ -31,6 +32,7 @@ export namespace Icon {
3132
| 'MISTRAL'
3233
| 'GROK'
3334
| 'HUGGING_CHAT'
35+
| 'KIMI'
3436
| 'QWEN'
3537
| 'YUANBAO'
3638
| 'DOUBAO'
@@ -82,6 +84,9 @@ export const Icon: React.FC<Icon.Props> = ({ variant }) => {
8284
case 'HUGGING_CHAT':
8385
icon = <HuggingChat />
8486
break
87+
case 'KIMI':
88+
icon = <Kimi />
89+
break
8590
case 'QWEN':
8691
icon = <Qwen />
8792
break

packages/ui/src/constants/chatbot-to-icon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const chatbot_to_icon = {
1313
HuggingChat: 'HUGGING_CHAT',
1414
Qwen: 'QWEN',
1515
Yuanbao: 'YUANBAO',
16+
Kimi: 'KIMI',
1617
Doubao: 'DOUBAO',
1718
Perplexity: 'PERPLEXITY'
1819
} as Record<string, Icon.Variant>

0 commit comments

Comments
 (0)