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);
+ }
+});
-
-