410 lines
19 KiB
JavaScript
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完成初始化
|
|
}
|
|
});
|