diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap index 9309342614013..887807db97f00 100644 --- a/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap +++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap @@ -18,7 +18,7 @@ exports[`AskAssistantButton > renders button with unread messages correctly 1`] - Ask Assistant + n8n AI @@ -73,7 +73,7 @@ exports[`AskAssistantButton > renders default button correctly 1`] = ` - Ask Assistant + n8n AI diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts index 26911fe49330b..71f5ac14c8068 100644 --- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts @@ -1,7 +1,7 @@ import type { StoryFn } from '@storybook/vue3-vite'; import AskAssistantChat from './AskAssistantChat.vue'; -import type { ChatUI } from '../../types/assistant'; +import type { ChatUI, WorkflowSuggestion } from '../../types/assistant'; export default { title: 'Assistant/AskAssistantChat', @@ -21,7 +21,7 @@ const Template: StoryFn = (args, { argTypes }) => ({ components: { AskAssistantChat, }, - template: '
', + template: '
', methods, }); @@ -33,6 +33,38 @@ DefaultPlaceholderChat.args = { }, }; +const mockSuggestions: WorkflowSuggestion[] = [ + { + id: 'invoice-pipeline', + summary: 'Invoice processing pipeline', + prompt: + 'Create an invoice parsing workflow using n8n forms. Extract key information and store in Airtable.', + }, + { + id: 'ai-news-digest', + summary: 'Daily AI news digest', + prompt: + 'Create a workflow that fetches the latest AI news every morning at 8 AM and sends a summary via Telegram.', + }, + { + id: 'rag-assistant', + summary: 'RAG knowledge assistant', + prompt: + 'Build a pipeline that accepts PDF files, chunks documents, and creates a chatbot that can answer questions.', + }, +]; + +export const WithSuggestions = Template.bind({}); +WithSuggestions.args = { + user: { + firstName: 'Max', + lastName: 'Test', + }, + suggestions: mockSuggestions, + creditsQuota: 100, + creditsRemaining: 75, +}; + export const Chat = Template.bind({}); Chat.args = { user: { diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue index 6e332da020c86..c439cf2b45bda 100644 --- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue +++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue @@ -1,9 +1,9 @@ + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap new file mode 100644 index 0000000000000..bcde22c6e542f --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap @@ -0,0 +1,48 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`N8nPromptInputSuggestions > renders correctly with default props 1`] = ` +
+
+ +
+ + +
+ +
+ + + + + +
+ + + +
+
+`; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts new file mode 100644 index 0000000000000..395ad24f87b86 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts @@ -0,0 +1,4 @@ +import N8nPromptInputSuggestions from './N8nPromptInputSuggestions.vue'; + +export default N8nPromptInputSuggestions; +export type { WorkflowSuggestion } from '../../types/assistant'; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue b/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue index f21c947654eaf..4231be836ce83 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue @@ -6,7 +6,7 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from 'reka-ui'; -import { computed } from 'vue'; +import { computed, ref, nextTick, type Ref } from 'vue'; export interface Props { /** @@ -58,6 +58,13 @@ const props = withDefaults(defineProps(), { asChild: false, }); +// Type for the ScrollAreaRoot instance with the viewport property +interface ScrollAreaRootWithViewport { + viewport?: Ref | HTMLElement; +} + +const rootRef = ref(); + const viewportStyle = computed(() => { const style: Record = {}; if (props.maxHeight) { @@ -68,10 +75,96 @@ const viewportStyle = computed(() => { } return style; }); + +/** + * Gets the viewport element from the root ref + */ +function getViewportElement(): HTMLElement | undefined { + if (!rootRef.value?.viewport) return undefined; + + const viewport = rootRef.value.viewport; + + // If it's a Vue ref, unwrap it + if (typeof viewport === 'object' && 'value' in viewport) { + return viewport.value; + } + + // If it's already an HTMLElement, use it directly + if (viewport instanceof HTMLElement) { + return viewport; + } + + return undefined; +} + +/** + * Scrolls the viewport to the bottom + * @param options - Options for controlling scroll behavior + */ +async function scrollToBottom(options: { smooth?: boolean } = {}) { + // Wait for DOM updates to ensure content is fully rendered + await nextTick(); + + const viewport = getViewportElement(); + + if (viewport && typeof viewport.scrollTo === 'function') { + viewport.scrollTo({ + top: viewport.scrollHeight, + behavior: options.smooth ? 'smooth' : 'auto', + }); + } else if (viewport) { + // Fallback for test environments or browsers that don't support scrollTo + viewport.scrollTop = viewport.scrollHeight; + } +} + +/** + * Scrolls the viewport to the top + * @param options - Options for controlling scroll behavior + */ +async function scrollToTop(options: { smooth?: boolean } = {}) { + await nextTick(); + + const viewport = getViewportElement(); + + if (viewport && typeof viewport.scrollTo === 'function') { + viewport.scrollTo({ + top: 0, + behavior: options.smooth ? 'smooth' : 'auto', + }); + } else if (viewport) { + // Fallback for test environments or browsers that don't support scrollTo + viewport.scrollTop = 0; + } +} + +/** + * Gets the current scroll position + */ +function getScrollPosition() { + const viewport = getViewportElement(); + + if (viewport) { + return { + top: viewport.scrollTop, + left: viewport.scrollLeft, + height: viewport.scrollHeight, + width: viewport.scrollWidth, + }; + } + return null; +} + +defineExpose({ + scrollToBottom, + scrollToTop, + getScrollPosition, +});