diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index b171e21e0f37..4b8e5c9ff3f1 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -8218,6 +8218,13 @@ export interface components { } & { [key: string]: unknown; }; + /** ChatEntityContext */ + ChatEntityContext: { + /** Datasets */ + datasets?: components["schemas"]["EntityReference"][]; + /** Histories */ + histories?: components["schemas"]["EntityReference"][]; + }; /** ChatExchangeBatchDeletePayload */ ChatExchangeBatchDeletePayload: { /** @@ -8252,6 +8259,11 @@ export interface components { * @default */ context: string | null; + /** + * Entity Context + * @description Structured entity references resolved from @mentions in the query. + */ + entity_context?: components["schemas"]["ChatEntityContext"] | null; /** * Exchange ID * @description The ID of an existing chat exchange to continue. @@ -12016,6 +12028,36 @@ export interface components { */ src: components["schemas"]["DataItemSourceType"]; }; + /** EntityReference */ + EntityReference: { + /** Extension */ + extension?: string | null; + /** HID */ + hid?: number | null; + /** + * Entity ID + * @description The resolved encoded ID of the entity. + */ + id?: string | null; + /** + * Identifier + * @description The identifier as typed by the user (HID number or name). + */ + identifier: string; + /** + * Name + * @description The display name of the entity. + * @default + */ + name: string; + /** State */ + state?: string | null; + /** + * Entity Type + * @description The type of entity being referenced (e.g. 'dataset', 'history'). + */ + type: string; + }; /** ExitCodeJobMessage */ ExitCodeJobMessage: { /** Code Desc */ diff --git a/client/src/components/ActivityBar/ActivityBar.test.js b/client/src/components/ActivityBar/ActivityBar.test.js index 85e565ff92d9..be4088d80e02 100644 --- a/client/src/components/ActivityBar/ActivityBar.test.js +++ b/client/src/components/ActivityBar/ActivityBar.test.js @@ -22,6 +22,7 @@ vi.mock("@/composables/config", () => ({ vi.mock("vue-router/composables", () => ({ useRoute: vi.fn(() => ({})), + useRouter: vi.fn(() => ({ push: vi.fn() })), })); const { server, http } = useServerMock(); diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue index bd89c9fb8c42..3f823190e0be 100644 --- a/client/src/components/ActivityBar/ActivityBar.vue +++ b/client/src/components/ActivityBar/ActivityBar.vue @@ -4,13 +4,14 @@ import { faBell, faEllipsisH, faUserCog } from "@fortawesome/free-solid-svg-icon import { watchImmediate } from "@vueuse/core"; import { storeToRefs } from "pinia"; import { computed, type Ref, ref } from "vue"; -import { useRoute } from "vue-router/composables"; +import { useRoute, useRouter } from "vue-router/composables"; import draggable from "vuedraggable"; import { useConfig } from "@/composables/config"; import { convertDropData } from "@/stores/activitySetup"; import { useActivityStore } from "@/stores/activityStore"; import type { Activity } from "@/stores/activityStoreTypes"; +import { useChatStore } from "@/stores/chatStore"; import { useEventStore } from "@/stores/eventStore"; import { useUnprivilegedToolStore } from "@/stores/unprivilegedToolStore"; import { useUserStore } from "@/stores/userStore"; @@ -78,7 +79,9 @@ const DRAG_DELAY = 50; const { config, isConfigLoaded } = useConfig(); const route = useRoute(); +const router = useRouter(); const userStore = useUserStore(); +const chatStore = useChatStore(); const eventStore = useEventStore(); const activityStore = useActivityStore(props.activityBarId); @@ -173,6 +176,9 @@ function isActiveSideBar(menuKey: string) { * Checks if an activity that has a panel should have the `is-active` prop */ function panelActivityIsActive(activity: Activity) { + if (activity.id === "chatgxy" && !chatStore.isCenterMode && chatStore.chatVisible) { + return true; + } return isActiveSideBar(activity.id) || isActiveRoute(activity.to); } @@ -232,6 +238,19 @@ function toggleSidebar(toggle: string = "", to: string | null = null) { activityStore.toggleSideBar(toggle); } +function onChatGxyClick() { + if (chatStore.isCenterMode) { + toggleSidebar("chatgxy"); + if (route.path.startsWith("/chatgxy")) { + router.push("/"); + } else { + router.push("/chatgxy"); + } + } else { + chatStore.toggleChat(); + } +} + function onActivityClicked(activity: Activity) { if (activity.click) { emit("activityClicked", activity.id); @@ -301,6 +320,16 @@ defineExpose({ :tooltip="activity.tooltip" :to="activity.to" @click="toggleSidebar(activity.id, activity.to)" /> + -import { faExternalLinkAlt, faMagic, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + faAngleDoubleDown, + faColumns, + faExpand, + faExternalLinkAlt, + faFile, + faMagic, + faPlus, + faSitemap, + faTimes, + faTrash, + faWrench, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BSkeleton } from "bootstrap-vue"; -import { nextTick, onMounted, ref, watch } from "vue"; +import { computed, nextTick, onMounted, ref, watch } from "vue"; +import { useRoute, useRouter } from "vue-router/composables"; import { GalaxyApi } from "@/api"; import { getGalaxyInstance } from "@/app"; import { type AgentResponse, useAgentActions } from "@/composables/agentActions"; import { useMarkdown } from "@/composables/markdown"; +import { useActiveContext } from "@/composables/useActiveContext"; +import { buildEntityContext, parseMentions, resolveMentions } from "@/composables/useEntityMentions"; +import { useChatStore } from "@/stores/chatStore"; import { errorMessageAsString } from "@/utils/simple-error"; import { getAgentIcon } from "./ChatGXY/agentTypes"; @@ -22,13 +38,54 @@ const props = withDefaults( defineProps<{ exchangeId?: string; compact?: boolean; + docked?: boolean; + panel?: boolean; }>(), { exchangeId: undefined, compact: false, + docked: false, + panel: false, }, ); +const emit = defineEmits<{ + (e: "close"): void; + (e: "undock"): void; +}>(); + +const route = useRoute(); +const router = useRouter(); +const chatStore = useChatStore(); + +const { activeContext, contextLabel } = useActiveContext(); +const contextDismissed = ref(false); + +watch(activeContext, () => { + contextDismissed.value = false; +}); + +const effectiveContext = computed(() => { + if (contextDismissed.value || (!props.docked && !props.panel)) { + return null; + } + return activeContext.value; +}); + +const contextIcon = computed(() => { + switch (effectiveContext.value?.contextType) { + case "tool": + return faWrench; + case "dataset": + return faFile; + case "workflow_editor": + case "workflow_run": + return faSitemap; + default: + return faMagic; + } +}); + const query = ref(""); const messages = ref([]); const busy = ref(false); @@ -43,6 +100,8 @@ const { processingAction, handleAction } = useAgentActions(); onMounted(async () => { if (props.exchangeId) { await loadChatById(props.exchangeId); + } else if (props.docked || props.panel) { + startNewChat(); } else { await loadLatestChat(); } @@ -104,6 +163,10 @@ async function submitQuery() { busy.value = true; try { + const parsed = parseMentions(currentQuery); + const resolved = resolveMentions(parsed); + const entityContext = buildEntityContext(resolved); + const { data, error } = await GalaxyApi().POST("/api/chat", { params: { query: { @@ -112,8 +175,9 @@ async function submitQuery() { }, body: { query: currentQuery, - context: null, + context: effectiveContext.value ? JSON.stringify(effectiveContext.value) : null, exchange_id: currentChatId.value, + entity_context: entityContext, }, }); @@ -288,6 +352,7 @@ async function loadLatestChat() { } function startNewChat() { + hasLoadedInitialChat.value = true; messages.value = [ { id: generateId(), @@ -302,6 +367,9 @@ function startNewChat() { ]; currentChatId.value = null; query.value = ""; + if (props.docked || props.panel) { + chatStore.setActiveChatId(null); + } } async function deleteCurrentChat() { @@ -326,11 +394,47 @@ function popOutToScratchbook() { const url = `${path}?compact=true`; Galaxy.frame.add({ title: "ChatGXY", url }); } + +function dockTo(location: "right" | "bottom") { + chatStore.setActiveChatId(currentChatId.value); + chatStore.setLocation(location); + chatStore.showChat(); + if (route.path.startsWith("/chatgxy")) { + router.push("/"); + } +} + +watch(currentChatId, (newId) => { + if (props.docked || props.panel) { + chatStore.setActiveChatId(newId); + } +});