| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- /* global $, userID, postData */
- /* eslint-env browser */
- 'use strict';
- (function() {
- $(function() {
- const root = $('#global-assistant');
- if (root.length === 0) return;
- const panel = root.find('.global-assistant-panel');
- const messages = root.find('.global-assistant-messages');
- const suggestions = root.find('.global-assistant-suggestions');
- const input = root.find('.global-assistant-input');
- const sendButton = root.find('.global-assistant-send');
- const entry = root.find('.global-assistant-entry');
- const projectId = root.data('project-id');
- const storageKey = 'global-assistant-history-v2-' + projectId + '-' + userID;
- const entryPositionKey = 'global-assistant-entry-position-v2-' + userID;
- let history = loadHistory();
- let conversationId = getConversationId();
- let sending = false;
- let entryPosition = loadEntryPosition();
- let entryDrag = null;
- let suppressEntryClick = false;
- let suppressClickTimer = null;
- let entryExpanded = false;
- applyEntryPosition();
- entry.on('click', function(event) {
- if (suppressEntryClick) {
- event.preventDefault();
- suppressEntryClick = false;
- return;
- }
- panel.addClass('show').attr('aria-hidden', 'false');
- $(this).attr('aria-expanded', 'true').hide();
- input.trigger('focus');
- scrollToBottom();
- });
- entry.on('pointerdown', function(event) {
- const pointer = event.originalEvent;
- if (pointer.pointerType === 'mouse' && pointer.button !== 0) return;
- if (entryDrag) return;
- const rect = this.getBoundingClientRect();
- entryDrag = {
- pointerId: pointer.pointerId,
- startX: pointer.clientX,
- startY: pointer.clientY,
- grabRatio: rect.width ? (pointer.clientX - rect.left) / rect.width : 0.5,
- grabY: pointer.clientY - rect.top,
- originalPosition: Object.assign({}, entryPosition),
- moved: false,
- };
- clearTimeout(suppressClickTimer);
- suppressEntryClick = false;
- this.setPointerCapture(pointer.pointerId);
- event.preventDefault();
- });
- entry.on('pointermove', function(event) {
- const pointer = event.originalEvent;
- if (!entryDrag || pointer.pointerId !== entryDrag.pointerId) return;
- const offsetX = pointer.clientX - entryDrag.startX;
- const offsetY = pointer.clientY - entryDrag.startY;
- if (!entryDrag.moved && Math.abs(offsetX) <= 4 && Math.abs(offsetY) <= 4) return;
- entryDrag.moved = true;
- entryExpanded = false;
- entry.addClass('dragging');
- const width = 92;
- entryPosition = clampEntryPosition({
- right: window.innerWidth - pointer.clientX - width * (1 - entryDrag.grabRatio),
- top: pointer.clientY - entryDrag.grabY,
- docked: false,
- });
- applyEntryPosition();
- event.preventDefault();
- });
- entry.on('pointerup pointercancel lostpointercapture', function(event) {
- const pointer = event.originalEvent;
- if (!entryDrag || pointer.pointerId !== entryDrag.pointerId) return;
- const drag = entryDrag;
- entryDrag = null;
- suppressEntryClick = event.type === 'pointerup' && drag.moved;
- if (event.type !== 'pointerup') entryPosition = drag.originalPosition;
- else if (drag.moved) entryPosition.docked = entryPosition.right <= 24;
- if (drag.moved || event.type !== 'pointerup') entryExpanded = false;
- entryPosition = clampEntryPosition(entryPosition);
- entry.removeClass('dragging');
- applyEntryPosition();
- if (this.hasPointerCapture(pointer.pointerId)) this.releasePointerCapture(pointer.pointerId);
- saveEntryPosition();
- suppressClickTimer = setTimeout(function() { suppressEntryClick = false; }, 0);
- });
- entry.on('pointerenter focusin', function(event) {
- if (!entryPosition.docked || entryDrag || (event.originalEvent && event.originalEvent.pointerType === 'touch')) return;
- entryExpanded = true;
- applyEntryPosition();
- });
- entry.on('pointerleave focusout', function() {
- if (entryDrag) return;
- entryExpanded = false;
- applyEntryPosition();
- });
- $(window).on('resize.globalAssistant', function() {
- entryPosition = clampEntryPosition(entryPosition);
- applyEntryPosition();
- saveEntryPosition();
- });
- root.find('.global-assistant-close').on('click', function() {
- panel.removeClass('show').attr('aria-hidden', 'true');
- entryExpanded = false;
- entry.attr('aria-expanded', 'false').show();
- applyEntryPosition();
- });
- root.find('.global-assistant-clear').on('click', function() {
- if (sending) return;
- history = [];
- conversationId = '';
- saveHistory();
- renderHistory();
- });
- root.find('.global-assistant-form').on('submit', function(event) {
- event.preventDefault();
- ask(input.val());
- });
- input.on('keydown', function(event) {
- if (event.key === 'Enter' && !event.shiftKey) {
- event.preventDefault();
- root.find('.global-assistant-form').trigger('submit');
- }
- });
- suggestions.on('click', 'button', function() {
- ask($(this).text());
- });
- renderHistory();
- function ask(question) {
- question = $.trim(question || '');
- if (!question || sending) return;
- input.val('');
- addHistory({ role: 'user', text: question });
- setSending(true);
- postData('/assistant/query/v2', {
- question,
- conversationId,
- context: {
- path: window.location.pathname,
- title: document.title,
- subProjectId: root.data('sub-project-id') || '',
- tenderId: getTenderId(),
- },
- }, function(data) {
- conversationId = data.conversationId || '';
- addHistory({ role: 'assistant', text: data.answer, data });
- setSending(false);
- }, function(message) {
- addHistory({ role: 'assistant', text: message || '查询失败,请稍后重试。' });
- setSending(false);
- }, false, '助手查询失败');
- }
- function setSending(value) {
- sending = value;
- input.prop('disabled', value);
- sendButton.prop('disabled', value);
- root.find('.global-assistant-clear').prop('disabled', value);
- messages.find('.global-assistant-loading').remove();
- if (value) {
- const loading = $('<div class="global-assistant-message assistant global-assistant-loading"></div>');
- loading.append($('<div class="global-assistant-bubble"></div>').text('正在查找相关内容…'));
- messages.append(loading);
- }
- scrollToBottom();
- if (!value) input.trigger('focus');
- }
- function addHistory(message) {
- history.push(message);
- if (history.length > 20) history = history.slice(history.length - 20);
- saveHistory();
- renderHistory();
- }
- function renderHistory() {
- messages.empty();
- if (history.length === 0) {
- appendMessage({
- role: 'assistant',
- text: '你好,我可以查询当前账号有权查看的业务数据,也可以从用户手册中查找操作说明。',
- });
- renderSuggestions(['查询平台概况', '查询项目', '如何创建标段', '怎么设置审批流程']);
- } else {
- history.forEach(appendMessage);
- const lastAssistant = history.slice().reverse().find(item => item.role === 'assistant' && item.data);
- renderSuggestions(lastAssistant && lastAssistant.data.suggestions ? lastAssistant.data.suggestions : []);
- }
- scrollToBottom();
- }
- function appendMessage(message) {
- const wrapper = $('<div class="global-assistant-message"></div>').addClass(message.role);
- const bubble = $('<div class="global-assistant-bubble"></div>').text(message.text || '');
- wrapper.append(bubble);
- if (message.role === 'assistant' && message.data) {
- appendSummary(bubble, message.data.summary || []);
- appendItems(bubble, message.data.items || []);
- if (message.data.truncated) {
- bubble.append($('<div class="global-assistant-more"></div>').text('结果较多,仅展示前20条。请增加名称或编号缩小范围。'));
- }
- if (message.data.tenderMatchNotice) {
- bubble.append($('<div class="global-assistant-tender-notice"></div>').text(message.data.tenderMatchNotice));
- }
- }
- messages.append(wrapper);
- }
- function appendSummary(bubble, summary) {
- if (!summary.length) return;
- const summaryBox = $('<div class="global-assistant-summary"></div>');
- summary.forEach(function(item) {
- const block = $('<div></div>');
- block.append($('<span></span>').text(item.label));
- block.append($('<strong></strong>').text(item.value));
- summaryBox.append(block);
- });
- bubble.append(summaryBox);
- }
- function appendItems(bubble, items) {
- if (!items.length) return;
- const list = $('<div class="global-assistant-results"></div>');
- items.forEach(function(item) {
- if (item.pageUrl) {
- const row = $('<div class="global-assistant-result"></div>');
- const info = $('<div class="global-assistant-result-info"></div>');
- info.append($('<strong></strong>').text(item.title || '未命名记录'));
- if (item.description) info.append($('<small></small>').text(item.description));
- row.append(info);
- if (item.value) row.append($('<span class="global-assistant-result-value"></span>').text(item.value));
- const actions = $('<div class="global-assistant-result-actions"></div>');
- actions.append($('<a></a>').attr({ href: item.pageUrl, title: '前往页面', 'aria-label': '前往页面' })
- .append($('<i class="fa fa-external-link" aria-hidden="true"></i>')));
- if (item.url) {
- actions.append($('<a></a>').attr({ href: item.url, title: '下载', 'aria-label': '下载' })
- .append($('<i class="fa fa-download" aria-hidden="true"></i>')));
- }
- row.append(actions);
- list.append(row);
- return;
- }
- const isExternal = /^https?:\/\//i.test(item.url || '');
- const row = $('<a></a>')
- .attr('href', item.url || 'javascript:void(0)')
- .attr('target', isExternal ? '_blank' : '_self');
- if (isExternal) row.attr('rel', 'noopener noreferrer');
- const info = $('<div></div>');
- info.append($('<strong></strong>').text(item.title || '未命名记录'));
- if (item.description) info.append($('<small></small>').text(item.description));
- row.append(info);
- if (item.value) row.append($('<span></span>').text(item.value));
- list.append(row);
- });
- bubble.append(list);
- }
- function renderSuggestions(list) {
- suggestions.empty();
- list.slice(0, 4).forEach(function(text) {
- suggestions.append($('<button type="button"></button>').text(text));
- });
- }
- function getTenderId() {
- const match = window.location.pathname.match(/^\/tender\/(\d+)/);
- return match ? match[1] : '';
- }
- function scrollToBottom() {
- messages.scrollTop(messages.prop('scrollHeight'));
- }
- function loadHistory() {
- try {
- const value = JSON.parse(localStorage.getItem(storageKey) || '[]');
- return value instanceof Array ? value : [];
- } catch (err) {
- return [];
- }
- }
- function saveHistory() {
- try {
- localStorage.setItem(storageKey, JSON.stringify(history));
- } catch (err) {
- // 浏览器禁用本地存储时仅保留当前页面内的对话。
- }
- }
- function getConversationId() {
- const last = history.slice().reverse().find(item => item.role === 'assistant' && item.data);
- return last && last.data.conversationId ? last.data.conversationId : '';
- }
- function loadEntryPosition() {
- const initial = { right: 0, top: window.innerHeight - 44 - 24, docked: true };
- try {
- const value = JSON.parse(localStorage.getItem(entryPositionKey) || '{}');
- if (!value || typeof value.docked !== 'boolean') return clampEntryPosition(initial);
- return clampEntryPosition(value);
- } catch (err) {
- return clampEntryPosition(initial);
- }
- }
- function saveEntryPosition() {
- try {
- localStorage.setItem(entryPositionKey, JSON.stringify(entryPosition));
- } catch (err) {
- // 浏览器禁用本地存储时仅在当前页面保留按钮位置。
- }
- }
- function applyEntryPosition() {
- entry.toggleClass('docked', entryPosition.docked)
- .toggleClass('expanded', entryPosition.docked && entryExpanded)
- .css({ right: entryPosition.right, top: entryPosition.top, transform: 'none' });
- }
- function clampEntryPosition(position) {
- const docked = position.docked === true;
- const maxRight = Math.max(0, window.innerWidth - 92 - 8);
- const maxTop = Math.max(0, window.innerHeight - 44 - 8);
- return {
- right: docked ? 0 : Math.min(maxRight, Math.max(Math.min(8, maxRight), Number(position.right) || 0)),
- top: Math.min(maxTop, Math.max(Math.min(8, maxTop), Number(position.top) || 0)),
- docked,
- };
- }
- });
- })();
|