Files
2025-02-20 06:42:40 +08:00

410 lines
19 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from '../../../scripts/api.js'
// custom_nodes/SemiChat-ComfyUI/web/index.js
app.registerExtension({
name: "SemiChatExtension",
async setup() {
// 依赖加载器
const loadDependencies = () => {
return new Promise((resolve) => {
const basicJS = "./extensions/SemiChat-ComfyUI/static/js"
const resources = [
{
id: 'babel',
src:`${basicJS}/babel.min.jsx`,
//src: 'https://unpkg.com/babel-standalone@6.15.0/babel.min.js',
check: () => !!window.Babel
},
{
id: 'ollama-js-client-global',
src:`${basicJS}/ollama-js.global.jsx`,
//src: 'https://gcore.jsdelivr.net/npm/ollama-js-client@1.0.2/dist/browser/iife/ollama-js.global.js',
check: () => !!window.OllamaJS
},
{
id:'ollama-js-client-jsonstore-global',
src:`${basicJS}/ollama-js-json-store.global.jsx`,
//src:'https://gcore.jsdelivr.net/npm/ollama-js-client@1.0.2/dist/browser/iife/ollama-js-json-store.global.js',
check: () => !!window.OllamaJS_json_store
},
];
let loadedCount = 0;
const totalNeeded = resources.length;
const checkComplete = () => {
if (++loadedCount === totalNeeded) {
// 加载字体和样式
const basicCSS = "./extensions/SemiChat-ComfyUI/static/css"
const semiCSS = document.createElement('link');
semiCSS.rel = 'stylesheet';
//semiCSS.href = 'https://unpkg.com/@douyinfe/semi-ui@2.73.0/dist/css/semi.css';
semiCSS.href = `${basicCSS}/semi.css`
document.head.appendChild(semiCSS);
const semiIcons = document.createElement('link');
semiIcons.rel = 'stylesheet';
//semiIcons.href = 'https://unpkg.com/@douyinfe/semi-icons@latest/dist/css/semi-icons.css';
semiIcons.href = `${basicCSS}/semi-icons.css`
document.head.appendChild(semiIcons);
resolve();
}
};
resources.forEach(res => {
if (!res.check()) {
const script = document.createElement('script');
script.src = res.src;
script.id = res.id;
if (res.id=="ollama-js-client-index"){
script.type = res.type;
}
if (res.id=="ollama-js-client-jsonstore-index"){
script.type = res.type;
}
script.onload = checkComplete;
script.onerror = () => {
console.error(`Failed to load ${res.id}`);
checkComplete();
};
document.head.appendChild(script);
} else {
checkComplete();
}
});
});
};
// 聊天组件容器
const createChatRoot = () => {
const existing = document.getElementById('semi-chat-root');
if (existing) return existing;
const container = document.createElement('div');
container.id = 'semi-chat-root';
container.style.position = 'absolute';
container.style.overflow = 'hidden';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '9999';
//const parentELE = document.querySelector(".graph-canvas-container")
//const parentELE = document.getElementById("vue-app")
//parentELE.appendChild(container)
document.body.appendChild(container);
return container;
};
const createChatComponent = () => {
const { Chat, SideSheet,Banner, Row, Collapse, MarkdownRender} = window.SemiUI;
const React = window.React;
const { useState, useCallback } = React;
const Ollama = window.OllamaJS;
const JSONStore = window.OllamaJS_json_store
const store = new JSONStore("semichat_data.json")
return function ChatApp() {
const [visible, setVisible] = useState(true);
const [message, setMessage] = useState([]);
const [roleInfo, setRoleinfo] = useState({});
const [banner,setBanner] = useState({});
const defaultMessage = [{
role:"assistant",
id:Date.now(),
createAt: Date.now(),
content:"您好,请问有什么可以帮您?",
status:"complete"
}];
const defaultBanner = {
show:false,
type:"danger",
description:"失去链接请重新双击.bat文件启动Ollama后端服务"
}
const showTemporaryBanner = (description) => {
setBanner({
show: true,
type: "info",
description,
});
setTimeout(() => setBanner(prev => ({ ...prev, show: false })), 1000);
};
if(message.length==0){
fetch("http://localhost:11434/api/ps")
.then((response) => response.json())
.then((data) => {
const handleSingleModel = (model) => {
setBanner(prev => ({ ...prev, show: false }));
const handleExistingSession = () => {
const store_message = JSON.parse(store.get("message"));
const store_roleInfo = JSON.parse(store.get("roleInfo"));
setRoleinfo(store_roleInfo);
setMessage(store_message);
window.firstThink = true
showTemporaryBanner("已恢复历史聊天记录");
};
const initializeNewSession = () => {
store.clear();
const basicImg = './extensions/SemiChat-ComfyUI/static/images'
const roleInfo = {
user: {
name: "小白",
avatar:`${basicImg}/user.png`
//avatar: "https://img.icons8.com/?size=100&id=22057&format=png&color=000000",
},
assistant: {
name: model,
avatar:`${basicImg}/assistant.png`
//avatar: "https://img.icons8.com/?size=100&id=YZ7rIIQ17EGq&format=png&color=000000",
}
};
setRoleinfo(roleInfo);
setMessage(defaultMessage);
store.set("model", model);
};
const shouldRestoreSession = store.has("message") && JSON.parse(store.get("message")).length !=0;
shouldRestoreSession ? handleExistingSession() : initializeNewSession();
window.clientOllama = new Ollama({
model: store.get("model"),
url: "http://localhost:11434/api/"
});
};
// 主逻辑
if (data.models.length !== 1) {
setBanner({ ...defaultBanner, show: true });
return;
}
const { model } = data.models[0];
handleSingleModel(model);
});
};
const commonOuterStyle = {
border: '1px solid var(--semi-color-border)',
borderRadius: '12px',
overflow:"hidden",
bottom: "12px",
}
async function getPromptResponse (content){
await window.clientOllama.prompt_stream(content,(error,response)=>{
if (error) {
console.error(error)
}
else if (response.done) {
// done!
window.firstThink = true;
setMessage((message) => {
const lastMessage = message[message.length - 1];
const newLastMessage = {
...lastMessage,
status: 'complete',
}
return [...message.slice(0, -1), newLastMessage];
});
}
else {
//console.log(JSONparser(response));
var resContent= response.response
if (resContent.includes("think>")){
if (window.firstThink){
resContent = "别催,我在烧烤\n";
window.firstThink = false;
window.firstTime = Date.now()
}else{
window.firstThink = true;
resContent = `\n烧烤完毕,用时${Math.floor((Date.now() - window.firstTime) / 1000)}秒`;
}
}
setMessage((message) => {
const lastMessage = message[message.length - 1];
if (lastMessage.status=="incomplete"){
const newLastMessage = {
...lastMessage,
status: 'incomplete',
content:lastMessage.content + resContent,
}
return [...message.slice(0, -1), newLastMessage];
}else{
const newAssistantMessage = {
id:Date.now(),
role:'assistant',
status: 'incomplete',
createAt: Date.now(),
content:resContent
}
return [ ...message, newAssistantMessage];
}
})
}
});
}
const onMessageSend = useCallback((content, attachment) => {
window.curContent = content
store.set("curContent",content)
window.firstThink = true
getPromptResponse(content)
}, []);
const onChatsChange = useCallback((chats) => {
const lastMessage = chats[chats.length - 2];
if (lastMessage.status=="complete"){
setMessage(chats);
}else{
showTemporaryBanner("正在作答,不要发起请求")
}
}, []);
const onClear = useCallback(()=>{
store.download();
store.clear();
setMessage([]);
window.clientOllama.clear();
window.firstThink = true
showTemporaryBanner("已清空上下文")
});
const onMessageReset = useCallback((message) => {
window.firstThink = true
setMessage((message) => {
const lastMessage = message[message.length - 1];
const newLastMessage = {
...lastMessage,
status: 'incomplete',
content:""
}
return [...message.slice(0, -1), newLastMessage];
});
getPromptResponse(`请重新作答用户的问题:${window.curContent?window.curContent:store.get("curContent")}`);
});
const onStopGenerator = useCallback((message)=>{
setMessage((message) => {
const lastMessage = message[message.length - 1];
const newLastMessage = {
...lastMessage,
status: 'complete',
}
return [...message.slice(0, -1), newLastMessage];
});
});
const onCancel = useCallback( async (e)=>{
setVisible(false);
store.set("message",JSON.stringify(message));
store.set("roleInfo",JSON.stringify(roleInfo));
await store.save();
window.firstThink = true;
showTemporaryBanner("已保存当前聊天记录");
});
const renderInputArea = useCallback((props)=>{
const { detailProps } = props;
const { clearContextNode,inputNode, sendNode, onClick } = detailProps;
return React.createElement(
Row,{
style:{margin: '8px 8px', display: 'flex', flexDirection:'row',
alignItems: 'flex-end', borderRadius: 8,padding: 8, border: '1px solid var(--semi-color-border)'},
onClick:onClick,
},clearContextNode,inputNode,sendNode
)
});
const renderChatBoxAction = useCallback((props)=>{
const {defaultActionsObj} = props
const {copyNode,resetNode} = defaultActionsObj
return React.createElement(Row,{
style:{
display: 'flex',
flexDirection:'row',
alignItems: 'flex-end'
}
},copyNode,resetNode)
});
return React.createElement(
SideSheet,
{
//title: "由Deepseek-R1与超级面爸结对编程完成",
visible: visible,
onCancel: onCancel,
size:"medium",
mask:false,
style:{
top:"35px",
},
},
banner.show ? React.createElement(Banner,{
type:banner.type,
description:banner.description
}):null,
React.createElement(Chat, {
key:"left-right-bubble",
mode:"userBubble",
style:commonOuterStyle,
chats:message,
roleConfig:roleInfo,
onChatsChange:onChatsChange,
onMessageSend:onMessageSend,
onMessageReset:onMessageReset,
showClearContext:true,
onClear:onClear,
showStopGenerate:true,
onStopGenerator:onStopGenerator,
renderInputArea:renderInputArea,
chatBoxRenderConfig:{renderChatBoxAction:renderChatBoxAction},
})
);
};
};
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
// get the basic options
const options = original_getCanvasMenuOptions.apply(this, arguments);
options.push(null); // inserts a divider
options.push({
content: "SemiChat@超级面爸&&deepseek r1",
callback: async () => {
// do whatever
const root = document.getElementById('semi-chat-root');
if (root) {
const chatApp = root._reactRoot;
if (chatApp) {
chatApp.render(
React.createElement(createChatComponent(), {
key: Date.now()
})
);
}
}else{
const rootEl = createChatRoot();
// 使用React 18的createRoot API
const root = window.ReactDOM.createRoot(rootEl);
rootEl._reactRoot = root; // 保存引用
root.render(
React.createElement(createChatComponent())
);
}
}
})
return options;
}
// 初始化流程
setTimeout(async () => {
try {
await loadDependencies();
} catch (error) {
console.error('Chat initialization failed:', error);
}
}, 2000); // 延迟确保ComfyUI完成初始化
}
});