|
@@ -69,7 +69,7 @@
|
|
|
@download-attachment="handleAttachmentDownload"
|
|
@download-attachment="handleAttachmentDownload"
|
|
|
@agent-detail-click="handleAgentDetailClick"
|
|
@agent-detail-click="handleAgentDetailClick"
|
|
|
@locate-message="handleLocateMessage"
|
|
@locate-message="handleLocateMessage"
|
|
|
- @open-web="openWebTab"
|
|
|
|
|
|
|
+ @open-web="handleOpenWebLink"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<!-- 附件列表面板 -->
|
|
<!-- 附件列表面板 -->
|
|
@@ -89,7 +89,7 @@
|
|
|
@ask-user-cancel="handleAskUserCancel"
|
|
@ask-user-cancel="handleAskUserCancel"
|
|
|
@agent-detail-click="handleAgentDetailClick"
|
|
@agent-detail-click="handleAgentDetailClick"
|
|
|
@open-markdown-tab="openMarkdownPreviewTab"
|
|
@open-markdown-tab="openMarkdownPreviewTab"
|
|
|
- @open-web="openWebTab"
|
|
|
|
|
|
|
+ @open-web="handleOpenWebLink"
|
|
|
@delete-message="handleDeleteMessage"
|
|
@delete-message="handleDeleteMessage"
|
|
|
@regenerate="handleRegenerate"
|
|
@regenerate="handleRegenerate"
|
|
|
@edit-resend="handleEditResend"
|
|
@edit-resend="handleEditResend"
|
|
@@ -135,36 +135,33 @@
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 右侧辅助面板(标签页容器,与 chat-panel 并列,带滑入滑出动画) -->
|
|
|
|
|
- <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
|
|
|
|
|
- <!-- <RightPanelTabs
|
|
|
|
|
- v-if="currentRightPanels.tabs.length > 0"
|
|
|
|
|
- :tabs="currentRightPanels.tabs"
|
|
|
|
|
- :activeTabId="currentRightPanels.activeTabId"
|
|
|
|
|
- :messages="messages"
|
|
|
|
|
- @switch-tab="activateTab"
|
|
|
|
|
- @close-tab="closeTab"
|
|
|
|
|
- @save-success="handleSaveSuccess"
|
|
|
|
|
- @schedule-deleted="handleScheduleDeleted"
|
|
|
|
|
- @schedule-updated="handleScheduleUpdated"
|
|
|
|
|
- @open-schedule-session="handleOpenScheduleSession"
|
|
|
|
|
- @open-markdown-tab="openMarkdownPreviewTab"
|
|
|
|
|
- /> -->
|
|
|
|
|
- <RightPanelTabs
|
|
|
|
|
- :tabs="currentRightPanels.tabs"
|
|
|
|
|
- :activeTabId="currentRightPanels.activeTabId"
|
|
|
|
|
- :messages="messages"
|
|
|
|
|
- @switch-tab="activateTab"
|
|
|
|
|
- @close-tab="closeTab"
|
|
|
|
|
- @save-success="handleSaveSuccess"
|
|
|
|
|
- @schedule-deleted="handleScheduleDeleted"
|
|
|
|
|
- @schedule-updated="handleScheduleUpdated"
|
|
|
|
|
- @open-schedule-session="handleOpenScheduleSession"
|
|
|
|
|
- @open-markdown-tab="openMarkdownPreviewTab"
|
|
|
|
|
- />
|
|
|
|
|
- </Transition>
|
|
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 右侧辅助面板(标签页容器):
|
|
|
|
|
+ - 始终挂载,用 v-show 控制显隐;这样关闭二维标签后 iframe 仍保留,只隐藏不销毁;
|
|
|
|
|
+ - 处于技能/子智能体/定时任务面板时同样只隐藏;
|
|
|
|
|
+ - 关闭整个助手弹框时,由 Modal 的 destroyOnClose 真正销毁。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
|
|
|
|
|
+ <RightPanelTabs
|
|
|
|
|
+ v-show="activePanel === 'chat' && currentRightPanels.tabs.length > 0"
|
|
|
|
|
+ :tabs="currentRightPanels.tabs"
|
|
|
|
|
+ :activeTabId="currentRightPanels.activeTabId"
|
|
|
|
|
+ :messages="messages"
|
|
|
|
|
+ :model-preview-loaded="modelPreview.loaded"
|
|
|
|
|
+ :model-preview-visible="modelPreviewVisible"
|
|
|
|
|
+ :model-preview-route="modelPreview.route"
|
|
|
|
|
+ @switch-tab="activateTab"
|
|
|
|
|
+ @close-tab="closeTab"
|
|
|
|
|
+ @save-success="handleSaveSuccess"
|
|
|
|
|
+ @schedule-deleted="handleScheduleDeleted"
|
|
|
|
|
+ @schedule-updated="handleScheduleUpdated"
|
|
|
|
|
+ @open-schedule-session="handleOpenScheduleSession"
|
|
|
|
|
+ @open-markdown-tab="openMarkdownPreviewTab"
|
|
|
|
|
+ />
|
|
|
|
|
+ </Transition>
|
|
|
|
|
+
|
|
|
<!-- 技能面板:非管理员只读(面板内部按 isAdmin 隐藏/禁用写操作) -->
|
|
<!-- 技能面板:非管理员只读(面板内部按 isAdmin 隐藏/禁用写操作) -->
|
|
|
<div v-if="activePanel === 'skill'" class="right-panel full">
|
|
<div v-if="activePanel === 'skill'" class="right-panel full">
|
|
|
<SkillPanel :is-admin="isAdmin" @close="activePanel = 'chat'" />
|
|
<SkillPanel :is-admin="isAdmin" @close="activePanel = 'chat'" />
|
|
@@ -189,7 +186,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
- import { ref, nextTick, watch, computed, onBeforeUnmount } from 'vue';
|
|
|
|
|
|
|
+ import { ref, nextTick, watch, computed, onBeforeUnmount, reactive } from 'vue';
|
|
|
import dayjs from 'dayjs';
|
|
import dayjs from 'dayjs';
|
|
|
// [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):引入 getMe 用于按 is_admin 隐藏管理入口
|
|
// [审计-第四轮 M3] 重放自我方 fix/base-agent-token(M1/M2/M3):引入 getMe 用于按 is_admin 隐藏管理入口
|
|
|
import { getDetail, deleteSession, deleteMessage, StreamError, getMe } from '/@/views/ventAI/manageAssistent/api';
|
|
import { getDetail, deleteSession, deleteMessage, StreamError, getMe } from '/@/views/ventAI/manageAssistent/api';
|
|
@@ -205,8 +202,10 @@
|
|
|
import SchedulePanel from '/@/views/ventAI/manageAssistent/components/panels/SchedulePanel.vue';
|
|
import SchedulePanel from '/@/views/ventAI/manageAssistent/components/panels/SchedulePanel.vue';
|
|
|
import RightPanelTabs from '/@/views/ventAI/manageAssistent/components/panels/RightPanelTabs.vue';
|
|
import RightPanelTabs from '/@/views/ventAI/manageAssistent/components/panels/RightPanelTabs.vue';
|
|
|
import { getScheduleList } from '/@/views/ventAI/manageAssistent/api';
|
|
import { getScheduleList } from '/@/views/ventAI/manageAssistent/api';
|
|
|
- import type { AttachedFile, Message, ScheduleTask, Task } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
|
|
|
|
+ import type { AttachedFile, Message, ScheduleTask, SubAppRoute, Task } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
|
|
import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
|
|
|
|
|
+ import { parseModalPreviewLink } from '/@/views/ventAI/manageAssistent/lib/webLink';
|
|
|
|
|
+ import { locateDevice, locateDeviceReport, locateTunnel } from '/@/views/ventAI/manageAssistent/lib/modalPreviewBridge';
|
|
|
import { useChatStream } from '../hooks/useChatStream';
|
|
import { useChatStream } from '../hooks/useChatStream';
|
|
|
import { useChatTasks } from '../hooks/useChatTasks';
|
|
import { useChatTasks } from '../hooks/useChatTasks';
|
|
|
import { useChatPanels } from '../hooks/useChatPanels';
|
|
import { useChatPanels } from '../hooks/useChatPanels';
|
|
@@ -299,6 +298,7 @@
|
|
|
openMarkdownPreviewTab,
|
|
openMarkdownPreviewTab,
|
|
|
openFilePreview,
|
|
openFilePreview,
|
|
|
openWebTab,
|
|
openWebTab,
|
|
|
|
|
+ openModalPreviewTab,
|
|
|
handleOpenFilePreview,
|
|
handleOpenFilePreview,
|
|
|
handleSaveSuccess,
|
|
handleSaveSuccess,
|
|
|
getCurrentTaskFiles,
|
|
getCurrentTaskFiles,
|
|
@@ -315,6 +315,186 @@
|
|
|
const { loadHistoryAttachments, handleAttachmentPreview: previewAttachment, handleAttachmentDownload } = useChatAttachments();
|
|
const { loadHistoryAttachments, handleAttachmentPreview: previewAttachment, handleAttachmentDownload } = useChatAttachments();
|
|
|
const handleAttachmentPreview = (file: AttachedFile) => previewAttachment(file, openFilePreview);
|
|
const handleAttachmentPreview = (file: AttachedFile) => previewAttachment(file, openFilePreview);
|
|
|
|
|
|
|
|
|
|
+ // ===== 二维模型生命周期:单活模型 + 关闭隐藏 + 空闲超时销毁 =====
|
|
|
|
|
+ interface ModelPreviewCache {
|
|
|
|
|
+ loaded: boolean;
|
|
|
|
|
+ ownerSessionId: string;
|
|
|
|
|
+ modelId: string;
|
|
|
|
|
+ route: SubAppRoute | null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 标签关闭/切换到其他面板时模型只隐藏;超过该时长未再次显示才真正销毁
|
|
|
|
|
+ const MODEL_PREVIEW_IDLE_MS = 5 * 60 * 1000;
|
|
|
|
|
+ const modelPreview = reactive<ModelPreviewCache>({
|
|
|
|
|
+ loaded: false,
|
|
|
|
|
+ ownerSessionId: '',
|
|
|
|
|
+ modelId: '',
|
|
|
|
|
+ route: null,
|
|
|
|
|
+ });
|
|
|
|
|
+ let modelPreviewIdleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
+ let modelPreviewTransition: Promise<void> | null = null;
|
|
|
|
|
+ let pendingModelPreviewRoute: SubAppRoute | null = null;
|
|
|
|
|
+ let modelPreviewEpoch = 0;
|
|
|
|
|
+ let lastModelLocateSignature = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 当前会话归属:优先用后端 session_id,空白任务用前端任务 id 兜底
|
|
|
|
|
+ const currentSessionKey = (): string => {
|
|
|
|
|
+ const task = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
|
|
+ return task?.sessionId || task?.id || '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 只有“二维标签处于激活状态 + 聊天面板可见 + 模型已加载”才算可见
|
|
|
|
|
+ const modelPreviewVisible = computed(() => {
|
|
|
|
|
+ if (activePanel.value !== 'chat' || !modelPreview.loaded) return false;
|
|
|
|
|
+ const state = currentRightPanels.value;
|
|
|
|
|
+ const active = state.tabs.find((tab) => tab.id === state.activeTabId);
|
|
|
|
|
+ return active?.type === 'model2d';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const clearModelPreviewIdleTimer = () => {
|
|
|
|
|
+ if (modelPreviewIdleTimer !== null) {
|
|
|
|
|
+ clearTimeout(modelPreviewIdleTimer);
|
|
|
|
|
+ modelPreviewIdleTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const destroyModelPreview = () => {
|
|
|
|
|
+ modelPreviewEpoch++;
|
|
|
|
|
+ pendingModelPreviewRoute = null;
|
|
|
|
|
+ clearModelPreviewIdleTimer();
|
|
|
|
|
+ modelPreview.loaded = false;
|
|
|
|
|
+ modelPreview.modelId = '';
|
|
|
|
|
+ modelPreview.route = null;
|
|
|
|
|
+ modelPreview.ownerSessionId = '';
|
|
|
|
|
+ lastModelLocateSignature = '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const scheduleModelPreviewIdleDestroy = () => {
|
|
|
|
|
+ clearModelPreviewIdleTimer();
|
|
|
|
|
+ if (!modelPreview.loaded) return;
|
|
|
|
|
+ modelPreviewIdleTimer = setTimeout(() => {
|
|
|
|
|
+ modelPreviewIdleTimer = null;
|
|
|
|
|
+ destroyModelPreview();
|
|
|
|
|
+ }, MODEL_PREVIEW_IDLE_MS);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 同一模型下只做定位,不重载 iframe:
|
|
|
|
|
+ * tunid → 巷道定位,locationId → 设备定位,locationName → 报表定位。
|
|
|
|
|
+ */
|
|
|
|
|
+ const applyModelLocate = (route: SubAppRoute | null) => {
|
|
|
|
|
+ const query = route?.query || {};
|
|
|
|
|
+ const modelId = query.model_id || modelPreview.modelId;
|
|
|
|
|
+ if (!modelId) return;
|
|
|
|
|
+ const signature = [modelId, query.tunid || '', query.locationId || '', query.locationName || '', query.deviceType || ''].join('|');
|
|
|
|
|
+ if (signature === lastModelLocateSignature) return;
|
|
|
|
|
+ lastModelLocateSignature = signature;
|
|
|
|
|
+
|
|
|
|
|
+ const deviceType = query.deviceType || (query.tunid ? 'tun' : 'fanlocal');
|
|
|
|
|
+ if (query.tunid) {
|
|
|
|
|
+ locateTunnel({ model_id: modelId, tunid: query.tunid, deviceType });
|
|
|
|
|
+ } else if (query.locationId) {
|
|
|
|
|
+ locateDevice({ model_id: modelId, deviceType, locationId: query.locationId });
|
|
|
|
|
+ } else if (query.locationName) {
|
|
|
|
|
+ locateDeviceReport({ model_id: modelId, deviceType, locationName: query.locationName });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 确保当前加载的模型与目标路由一致:
|
|
|
|
|
+ * - 未加载:加载;
|
|
|
|
|
+ * - model_id 相同:只隐藏/显示,定位参数走 bridge,不重载;
|
|
|
|
|
+ * - model_id 不同:先卸载旧 iframe,下一帧再挂载新模型。
|
|
|
|
|
+ */
|
|
|
|
|
+ const ensureModelPreview = async (route: SubAppRoute | null) => {
|
|
|
|
|
+ if (!route) return;
|
|
|
|
|
+ const modelId = route.query?.model_id || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (modelPreview.loaded && modelPreview.modelId === modelId) {
|
|
|
|
|
+ modelPreview.ownerSessionId = currentSessionKey();
|
|
|
|
|
+ applyModelLocate(route);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pendingModelPreviewRoute = route;
|
|
|
|
|
+ if (modelPreviewTransition) return;
|
|
|
|
|
+
|
|
|
|
|
+ modelPreviewTransition = (async () => {
|
|
|
|
|
+ while (pendingModelPreviewRoute) {
|
|
|
|
|
+ const target = pendingModelPreviewRoute;
|
|
|
|
|
+ pendingModelPreviewRoute = null;
|
|
|
|
|
+ const targetModelId = target.query?.model_id || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (modelPreview.loaded && modelPreview.modelId === targetModelId) {
|
|
|
|
|
+ modelPreview.ownerSessionId = currentSessionKey();
|
|
|
|
|
+ applyModelLocate(target);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const epoch = modelPreviewEpoch;
|
|
|
|
|
+ clearModelPreviewIdleTimer();
|
|
|
|
|
+ modelPreview.loaded = false;
|
|
|
|
|
+ // 先让旧 iframe 真正卸载,再挂载新模型,避免同帧复用组件导致旧模型残留
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ if (epoch !== modelPreviewEpoch) return;
|
|
|
|
|
+
|
|
|
|
|
+ modelPreview.route = target;
|
|
|
|
|
+ modelPreview.modelId = targetModelId;
|
|
|
|
|
+ modelPreview.ownerSessionId = currentSessionKey();
|
|
|
|
|
+ lastModelLocateSignature = '';
|
|
|
|
|
+ modelPreview.loaded = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ })().finally(() => {
|
|
|
|
|
+ modelPreviewTransition = null;
|
|
|
|
|
+ if (pendingModelPreviewRoute) {
|
|
|
|
|
+ void ensureModelPreview(pendingModelPreviewRoute);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ await modelPreviewTransition;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 会话切换、标签激活/关闭、面板切换后,按当前激活标签同步模型状态
|
|
|
|
|
+ const syncModelPreviewForActiveTab = () => {
|
|
|
|
|
+ if (!props.visible || activePanel.value !== 'chat') return;
|
|
|
|
|
+ const state = currentRightPanels.value;
|
|
|
|
|
+ const active = state.tabs.find((tab) => tab.id === state.activeTabId);
|
|
|
|
|
+ if (active?.type !== 'model2d') return;
|
|
|
|
|
+ void ensureModelPreview(active.subAppRoute || { path: '/modal-preview' });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const openModelPreview = (route: SubAppRoute, title?: string) => {
|
|
|
|
|
+ openModalPreviewTab({ title: title || '二维模型', subAppRoute: route });
|
|
|
|
|
+ void ensureModelPreview(route);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏后开始 5 分钟空闲倒计时;重新显示立即取消
|
|
|
|
|
+ watch(modelPreviewVisible, (visible) => {
|
|
|
|
|
+ if (visible) clearModelPreviewIdleTimer();
|
|
|
|
|
+ else scheduleModelPreviewIdleDestroy();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => [currentTaskId.value, currentRightPanels.value.activeTabId, activePanel.value] as const,
|
|
|
|
|
+ () => syncModelPreviewForActiveTab()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 聊天链接分流:链接中包含 modal-preview 时打开二维模型标签,并把链接参数传给子应用;
|
|
|
|
|
+ * 普通 http(s) 链接仍走右侧 Web 面板。
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleOpenWebLink = (payload: { url: string; title?: string }) => {
|
|
|
|
|
+ const route = parseModalPreviewLink(payload.url);
|
|
|
|
|
+ if (route) {
|
|
|
|
|
+ openModelPreview(route, payload.title);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ openWebTab(payload);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 二维模型标签/模型可由业务入口按需打开(宿主可通过组件 ref 调用)
|
|
|
|
|
+ defineExpose({ openModalPreviewTab, openModelPreview });
|
|
|
|
|
+
|
|
|
// —— 拖拽/居中职责已下沉到 useChatDrag(与面板全屏共用 lib/modalWrap) ——
|
|
// —— 拖拽/居中职责已下沉到 useChatDrag(与面板全屏共用 lib/modalWrap) ——
|
|
|
const drag = useChatDrag({
|
|
const drag = useChatDrag({
|
|
|
getIsFullscreen: () => isFullscreen.value,
|
|
getIsFullscreen: () => isFullscreen.value,
|
|
@@ -708,6 +888,11 @@
|
|
|
if (task.sessionId) {
|
|
if (task.sessionId) {
|
|
|
await deleteSession(task.sessionId);
|
|
await deleteSession(task.sessionId);
|
|
|
}
|
|
}
|
|
|
|
|
+ // 删除的是当前已加载模型所属的会话:直接销毁模型(而不是等空闲超时)
|
|
|
|
|
+ const ownerKey = task.sessionId || task.id;
|
|
|
|
|
+ if (modelPreview.ownerSessionId && modelPreview.ownerSessionId === ownerKey) {
|
|
|
|
|
+ destroyModelPreview();
|
|
|
|
|
+ }
|
|
|
const realIndex = taskList.value.findIndex((t) => t.id === task.id);
|
|
const realIndex = taskList.value.findIndex((t) => t.id === task.id);
|
|
|
if (realIndex !== -1) {
|
|
if (realIndex !== -1) {
|
|
|
taskList.value.splice(realIndex, 1);
|
|
taskList.value.splice(realIndex, 1);
|
|
@@ -951,7 +1136,12 @@
|
|
|
|
|
|
|
|
await nextTick(() => {
|
|
await nextTick(() => {
|
|
|
chatMessagesRef.value?.forceScrollToBottom();
|
|
chatMessagesRef.value?.forceScrollToBottom();
|
|
|
|
|
+ // 重新打开助手时,如果当前标签就是二维模型,按需重新加载
|
|
|
|
|
+ syncModelPreviewForActiveTab();
|
|
|
});
|
|
});
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 关闭整个助手弹框:模型立即销毁,而不是隐藏
|
|
|
|
|
+ destroyModelPreview();
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{ immediate: true }
|
|
{ immediate: true }
|
|
@@ -968,6 +1158,8 @@
|
|
|
activeFileReader.value.abort();
|
|
activeFileReader.value.abort();
|
|
|
activeFileReader.value = null;
|
|
activeFileReader.value = null;
|
|
|
}
|
|
}
|
|
|
|
|
+ // 清掉二维模型的空闲销毁定时器,避免卸载后仍回调
|
|
|
|
|
+ clearModelPreviewIdleTimer();
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|