import { ref, h, createApp, Teleport, type Directive, type App } from 'vue';
import Antd from 'ant-design-vue';
import { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
import type { TodoItem, ToolCallRecord, SSERawEvent } from './types';
import DataPickerModal from './components/DataPickerModal.vue';
import aiIcon from '@/assets/images/ventAI/dataPicker/AI-icon.svg';
// ─── 按钮 DOM 构建(悬停模式专用)──────────────────────────────────────────
const BUTTON_CLASS = 'di-floating-btn';
function createButtonEl(): HTMLElement {
const btn = document.createElement('div');
btn.className = BUTTON_CLASS;
btn.title = '数据解读';
btn.innerHTML = `
`;
Object.assign(btn.style, {
position: 'fixed',
zIndex: '99999',
padding: '8px',
background: 'linear-gradient(135deg, #1e90ff, #4da6ff)',
borderRadius: '50%',
cursor: 'pointer',
display: 'none',
pointerEvents: 'auto',
transition: 'transform 0.15s ease-out, opacity 0.15s ease-out',
transform: 'scale(0.9)',
opacity: '0',
boxShadow: '0 0 10px rgba(30,144,255,0.4)',
});
btn.addEventListener('mouseenter', () => {
const owner = (btn as any).__di_owner;
if (owner) {
owner.cancelLeave();
owner.clearHideTimer();
}
});
btn.addEventListener('mouseleave', () => {
const owner = (btn as any).__di_owner;
if (owner) owner.startHideTimer();
});
return btn;
}
// ─── 悬停触发器实例 ────────────────────────────────────────────────────────
const PAUSE_DELAY = 400;
const HIDE_DELAY = 3000;
function createHoverInstance(btn: HTMLElement) {
let pauseTimer: ReturnType | null = null;
let hideTimer: ReturnType | null = null;
let leaveTimer: ReturnType | null = null;
let visible = false;
let raw: any = null;
let hostEl: HTMLElement | null = null;
function setPosition(x: number, y: number) {
btn.style.left = x + 'px';
btn.style.top = y + 'px';
}
function detachMove() {
if (hostEl && inst.onMove) hostEl.removeEventListener('mousemove', inst.onMove);
}
function attachMove() {
if (hostEl && inst.onMove) hostEl.addEventListener('mousemove', inst.onMove);
}
function show() {
clearPauseTimer();
detachMove();
visible = true;
btn.style.display = 'block';
requestAnimationFrame(() => {
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
});
startHideTimer();
}
function hide() {
visible = false;
attachMove();
btn.style.display = 'none';
btn.style.transform = 'scale(0.9)';
btn.style.opacity = '0';
}
function clearPauseTimer() {
if (pauseTimer !== null) {
clearTimeout(pauseTimer);
pauseTimer = null;
}
}
function clearHideTimer() {
if (hideTimer !== null) {
clearTimeout(hideTimer);
hideTimer = null;
}
}
function startHideTimer() {
clearHideTimer();
hideTimer = setTimeout(hide, HIDE_DELAY);
}
const inst = {
get visible() {
return visible;
},
get raw() {
return raw;
},
setHost(el: HTMLElement) {
hostEl = el;
},
onEnter(e: MouseEvent) {
clearHideTimer();
if (!visible) {
setPosition(e.clientX - 36, e.clientY - 34);
clearPauseTimer();
pauseTimer = setTimeout(show, PAUSE_DELAY);
} else {
startHideTimer();
}
},
onMove(e: MouseEvent) {
setPosition(e.clientX - 36, e.clientY - 34);
clearPauseTimer();
pauseTimer = setTimeout(show, PAUSE_DELAY);
},
onLeave() {
clearPauseTimer();
clearHideTimer();
leaveTimer = setTimeout(hide, 150);
},
cancelLeave() {
if (leaveTimer !== null) {
clearTimeout(leaveTimer);
leaveTimer = null;
}
},
setRaw(val: any) {
raw = val;
},
clearHideTimer,
startHideTimer,
destroy() {
clearPauseTimer();
clearHideTimer();
if (leaveTimer !== null) {
clearTimeout(leaveTimer);
leaveTimer = null;
}
btn.remove();
},
};
return inst;
}
type HoverInstance = ReturnType;
// ─── 钩子主函数 ─────────────────────────────────────────────────────────────
export interface DataPickerOptions {
/** hover: 悬停出现浮动按钮;click: 点击元素直接触发 */
mode?: 'hover' | 'click';
}
export function useDataPicker(
mapParams: (
item: any
) => { tun_id: string; tun_name: string; mode?: string } | { device_id: string; device_name: string; device_type: string; mode?: string },
options: DataPickerOptions = {}
) {
console.log('useDataPicker', mapParams);
const { mode = 'hover' } = options;
const loading = ref(false);
const streamingText = ref('');
const error = ref(null);
const thinking = ref(false);
const todos = ref([]);
const executingTools = ref([]);
const toolCalls = ref([]);
const latestMessage = ref('');
const modalVisible = ref(false);
const itemName = ref('');
// 弹框容器 + 独立 Vue 应用
const modalContainer = document.createElement('div');
Object.assign(modalContainer.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: '1000',
});
document.body.appendChild(modalContainer);
let modalApp: App | null = null;
function mountModal() {
modalApp = createApp({
setup: () => ({
visible: modalVisible,
loading,
streamingText,
error,
thinking,
todos,
executingTools,
toolCalls,
latestMessage,
itemName,
onClose: () => {
modalVisible.value = false;
streamingText.value = '';
error.value = null;
thinking.value = false;
todos.value = [];
executingTools.value = [];
toolCalls.value = [];
latestMessage.value = '';
},
}),
render() {
return h(Teleport, { to: 'body' }, [
h(DataPickerModal, {
visible: this.visible,
loading: this.loading,
streamingText: this.streamingText,
error: this.error,
thinking: this.thinking,
todos: this.todos,
executingTools: this.executingTools,
toolCalls: this.toolCalls,
latestMessage: this.latestMessage,
itemName: this.itemName,
onClose: this.onClose,
}),
]);
},
});
modalApp.use(Antd);
modalApp.mount(modalContainer);
}
mountModal();
// SSE 事件处理
function handleEvent(event: SSERawEvent) {
switch (event.type) {
case 'thinking':
thinking.value = true;
break;
case 'executing':
thinking.value = false;
executingTools.value = event.tools || [];
if (event.message) latestMessage.value = event.message;
break;
case 'tool_call':
executingTools.value = [];
toolCalls.value = [...toolCalls.value, { tool: event.tool || '', source: event.source || 'main', status: 'call' as const }];
if (event.message) latestMessage.value = event.message;
break;
case 'tool_result':
if (toolCalls.value.length > 0) {
const last = toolCalls.value[toolCalls.value.length - 1];
if (last.tool === event.tool && last.status === 'call') last.status = 'result';
}
if (event.message) latestMessage.value = event.message;
break;
case 'updated_todo_list':
thinking.value = false;
todos.value = event.todos || [];
break;
case 'token':
thinking.value = false;
if (event.content) streamingText.value += event.content;
break;
}
}
// 点击触发
function handleClick(item: any) {
modalVisible.value = true;
loading.value = true;
error.value = null;
streamingText.value = '';
thinking.value = false;
todos.value = [];
executingTools.value = [];
toolCalls.value = [];
latestMessage.value = '';
const params = mapParams(item);
itemName.value = 'tun_name' in params ? params.tun_name : params.device_name;
const request = 'tun_id' in params ? fetchTunnelInterpretation(params, handleEvent) : fetchDeviceInterpretation(params, handleEvent);
request
.catch((e: any) => {
error.value = e?.message || '请求失败';
})
.finally(() => {
loading.value = false;
thinking.value = false;
executingTools.value = [];
});
}
// ── 悬停模式:按钮容器 + 触发器 ──
let btnContainer: HTMLDivElement | null = null;
const hoverMap = new Map();
if (mode === 'hover') {
btnContainer = document.createElement('div');
Object.assign(btnContainer.style, { position: 'fixed', top: '0', left: '0', zIndex: '99999', pointerEvents: 'none' });
document.body.appendChild(btnContainer);
}
function createHoverTrigger(): HoverInstance {
const btn = createButtonEl();
btnContainer!.appendChild(btn);
const inst = createHoverInstance(btn);
(btn as any).__di_owner = inst;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const item = (inst as any).raw;
if (item) handleClick(item);
});
return inst;
}
// ── 点击模式:元素映射 ──
const clickRawMap = new Map();
const clickHandlerMap = new Map void>();
// ── Vue 指令 ──
const directive: Directive = {
mounted(el, binding) {
if (mode === 'hover') {
const inst = createHoverTrigger();
inst.setHost(el);
inst.setRaw(binding.value);
hoverMap.set(el, inst);
el.addEventListener('mouseenter', inst.onEnter);
el.addEventListener('mousemove', inst.onMove);
el.addEventListener('mouseleave', inst.onLeave);
} else {
clickRawMap.set(el, binding.value);
const handler = (e: Event) => {
e.stopPropagation();
const item = clickRawMap.get(el);
if (item) handleClick(item);
};
clickHandlerMap.set(el, handler);
el.addEventListener('click', handler);
}
},
updated(el, binding) {
if (mode === 'hover') {
const inst = hoverMap.get(el);
if (inst) inst.setRaw(binding.value);
} else {
clickRawMap.set(el, binding.value);
}
},
unmounted(el) {
if (mode === 'hover') {
const inst = hoverMap.get(el);
if (inst) {
inst.destroy();
hoverMap.delete(el);
}
} else {
const handler = clickHandlerMap.get(el);
if (handler) {
el.removeEventListener('click', handler);
clickHandlerMap.delete(el);
}
clickRawMap.delete(el);
}
},
};
function cleanup() {
if (mode === 'hover') {
hoverMap.forEach((inst) => inst.destroy());
hoverMap.clear();
if (btnContainer) {
btnContainer.remove();
btnContainer = null;
}
} else {
clickRawMap.clear();
clickHandlerMap.forEach((handler, el) => el.removeEventListener('click', handler));
clickHandlerMap.clear();
}
if (modalApp) {
modalApp.unmount();
modalApp = null;
}
modalContainer.remove();
}
return { directive, cleanup, loading, streamingText, error, thinking, todos, executingTools, toolCalls, latestMessage, modalVisible };
}