mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
feat: Replaces WhatsApp support agent example with Telegram agent example (#35341)
This commit is contained in:
parent
c6917a194f
commit
e11a1e7d49
|
|
@ -1325,10 +1325,10 @@
|
|||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.scoreMyLeads.prompt": "When a new lead is created in my CRM, enrich it with Lemlist, score it based on fit, then update the lead if qualified and notify the sales team on Slack.",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.processInvoices.label": "Process invoices",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.processInvoices.prompt": "Every morning, scan Gmail for new invoices, use Claude to extract details and cross-check them against purchase orders in Google Sheets, flag any discrepancies for review, and add all payment due dates to Google Calendar automatically.",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.whatsappSupport.label": "WhatsApp support agent",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.whatsappSupport.prompt": "When a customer sends a WhatsApp message, use Gemini to match their question against our FAQ in Google Sheets and reply instantly. If it cannot resolve it, create a support ticket in Notion and alert the team on Slack.",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.scheduleSocialPosts.label": "Schedule social posts",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.scheduleSocialPosts.prompt": "Every Monday, read this week's content ideas from a Google Sheet, use Gemini to write tailored content then schedule them to post throughout the week.",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.telegramAgent.label": "Telegram support agent",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.telegramAgent.prompt": "Create a Telegram agent that answers customer questions using Claude, looks up order details in Gmail and our FAQ in Google Sheets, and escalates to a human when it is unsure.",
|
||||
"experiments.instanceAiWorkflowPreviewSuggestions.suggestions.seeAll": "See all",
|
||||
"experiments.personalizedTemplatesV3.browseAllTemplates": "Browse our template library",
|
||||
"experiments.personalizedTemplatesV3.couldntFind": "Need something different?",
|
||||
|
|
|
|||
|
|
@ -84,3 +84,45 @@ describe('WorkflowPreviewCanvas renderScale', () => {
|
|||
expect(style).toContain('scale(1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WorkflowPreviewCanvas full-canvas visualization', () => {
|
||||
const fullCanvasWorkflow: PreviewWorkflow = {
|
||||
nodes: [],
|
||||
connections: [],
|
||||
fullCanvasVisualization: { type: 'agent-builder' },
|
||||
};
|
||||
|
||||
function mountFullCanvas(animating: boolean) {
|
||||
vi.mocked(useElementSize).mockReturnValue({
|
||||
width: ref(1600),
|
||||
height: ref(420),
|
||||
} as unknown as ReturnType<typeof useElementSize>);
|
||||
|
||||
return mount(WorkflowPreviewCanvas, {
|
||||
props: { workflow: fullCanvasWorkflow, animating },
|
||||
global: { stubs: { WorkflowPreviewNode: true, AgentBuilderVisualization: true } },
|
||||
});
|
||||
}
|
||||
|
||||
it('renders the full-canvas visualization component', () => {
|
||||
const wrapper = mountFullCanvas(false);
|
||||
const viz = wrapper.findComponent({ name: 'AgentBuilderVisualization' });
|
||||
expect(viz.exists()).toBe(true);
|
||||
expect(viz.props('active')).toBe(false);
|
||||
});
|
||||
|
||||
it('activates the visualization when animating', async () => {
|
||||
const wrapper = mountFullCanvas(true);
|
||||
await wrapper.vm.$nextTick();
|
||||
const viz = wrapper.findComponent({ name: 'AgentBuilderVisualization' });
|
||||
expect(viz.props('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('stays active after the visualization completes', async () => {
|
||||
const wrapper = mountFullCanvas(true);
|
||||
const viz = wrapper.findComponent({ name: 'AgentBuilderVisualization' });
|
||||
viz.vm.$emit('complete');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.findComponent({ name: 'AgentBuilderVisualization' }).props('active')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import WorkflowPreviewNode from './WorkflowPreviewNode.vue';
|
|||
import SlackMessageVisualization from './visualizations/SlackMessageVisualization.vue';
|
||||
import SalesforceCardVisualization from './visualizations/SalesforceCardVisualization.vue';
|
||||
import InvoiceSpreadsheetVisualization from './visualizations/InvoiceSpreadsheetVisualization.vue';
|
||||
import WhatsAppChatVisualization from './visualizations/WhatsAppChatVisualization.vue';
|
||||
import AgentBuilderVisualization from './visualizations/AgentBuilderVisualization.vue';
|
||||
|
||||
const NODE_HALF_WIDTH = 48;
|
||||
const EDGE_CURVE_OFFSET = 60;
|
||||
|
|
@ -28,7 +28,7 @@ const visualizationComponents: Record<PreviewVisualizationType, Component> = {
|
|||
'slack-message': SlackMessageVisualization,
|
||||
'salesforce-card': SalesforceCardVisualization,
|
||||
'invoice-spreadsheet': InvoiceSpreadsheetVisualization,
|
||||
'whatsapp-chat': WhatsAppChatVisualization,
|
||||
'agent-builder': AgentBuilderVisualization,
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -40,13 +40,20 @@ const props = withDefaults(
|
|||
);
|
||||
|
||||
const canvasRef = ref<HTMLElement | null>(null);
|
||||
const { width: canvasRenderedWidth } = useElementSize(canvasRef);
|
||||
const { width: canvasRenderedWidth, height: canvasRenderedHeight } = useElementSize(canvasRef);
|
||||
|
||||
const animationPhase = ref<AnimationPhase>('idle');
|
||||
const nodeStates = reactive<Record<string, NodeAnimationState>>({});
|
||||
|
||||
const hasInputViz = computed(() => !!props.workflow.inputVisualization);
|
||||
const hasOutputViz = computed(() => !!props.workflow.outputVisualization);
|
||||
const hasFullCanvasViz = computed(() => !!props.workflow.fullCanvasVisualization);
|
||||
|
||||
const fullCanvasVizComponent = computed(() =>
|
||||
props.workflow.fullCanvasVisualization
|
||||
? visualizationComponents[props.workflow.fullCanvasVisualization.type]
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const outputVizItems = computed((): PreviewOutputVisualization[] => {
|
||||
const viz = props.workflow.outputVisualization;
|
||||
|
|
@ -147,7 +154,7 @@ async function runNodeAnimation() {
|
|||
function startAnimation() {
|
||||
stopped = false;
|
||||
resetStates();
|
||||
if (hasInputViz.value) {
|
||||
if (hasFullCanvasViz.value || hasInputViz.value) {
|
||||
animationPhase.value = 'input';
|
||||
} else {
|
||||
animationPhase.value = 'nodes';
|
||||
|
|
@ -169,6 +176,11 @@ function handleInputComplete() {
|
|||
void runNodeAnimation();
|
||||
}
|
||||
|
||||
function handleFullCanvasComplete() {
|
||||
if (stopped) return;
|
||||
animationPhase.value = 'done';
|
||||
}
|
||||
|
||||
function handleOutputComplete() {
|
||||
if (stopped) return;
|
||||
outputCompletedCount++;
|
||||
|
|
@ -291,7 +303,9 @@ const renderScale = computed(() =>
|
|||
|
||||
const contentStyle = computed(() => {
|
||||
const rs = renderScale.value;
|
||||
const verticalOffset = (CANVAS_HEIGHT * (1 - rs)) / 2;
|
||||
const renderedHeight =
|
||||
canvasRenderedHeight.value > 0 ? canvasRenderedHeight.value : CANVAS_HEIGHT;
|
||||
const verticalOffset = Math.max(0, (renderedHeight - CANVAS_HEIGHT * rs) / 2);
|
||||
return {
|
||||
width: `${CANVAS_WIDTH}px`,
|
||||
height: `${CANVAS_HEIGHT}px`,
|
||||
|
|
@ -436,6 +450,15 @@ function isEdgeSuccess(connection: PreviewWorkflowConnection): boolean {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="fullCanvasVizComponent" :class="$style.fullCanvasSlot">
|
||||
<component
|
||||
:is="fullCanvasVizComponent"
|
||||
:active="animationPhase !== 'idle'"
|
||||
v-bind="props.workflow.fullCanvasVisualization?.props"
|
||||
@complete="handleFullCanvasComplete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="inputVizComponent" :class="$style.vizSlot" :style="inputSlotStyle">
|
||||
<component
|
||||
:is="inputVizComponent"
|
||||
|
|
@ -472,7 +495,7 @@ function isEdgeSuccess(connection: PreviewWorkflowConnection): boolean {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
height: 420px;
|
||||
height: var(--workflow-preview-canvas-height, 420px);
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background-color: var(--canvas--color--background);
|
||||
|
|
@ -529,6 +552,15 @@ function isEdgeSuccess(connection: PreviewWorkflowConnection): boolean {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fullCanvasSlot {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vizSlotCompact {
|
||||
transform: translateY(-50%);
|
||||
transform-origin: left center;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import AgentBuilderVisualization from './AgentBuilderVisualization.vue';
|
||||
|
||||
function setPrefersReducedMotion(matches: boolean) {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn(() => ({ matches })),
|
||||
});
|
||||
}
|
||||
|
||||
describe('AgentBuilderVisualization', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
setPrefersReducedMotion(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('emits complete after the animation sequence finishes', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20000);
|
||||
|
||||
expect(wrapper.emitted('complete')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('fills in the builder sections as the animation progresses', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).not.toContain('Gmail');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20000);
|
||||
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('Gmail');
|
||||
expect(text).toContain('Google Sheets');
|
||||
expect(text).toContain('Summarize conversations');
|
||||
// The agent is not scheduled — the Schedules section stays empty.
|
||||
expect(text).toContain('Add schedule');
|
||||
expect(text).not.toContain('Daily digest');
|
||||
expect(text).toContain('Claude Opus 5');
|
||||
expect(text).toContain('escalate to a human');
|
||||
});
|
||||
|
||||
it('stops and resets when deactivated', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
await wrapper.setProps({ active: false });
|
||||
await vi.advanceTimersByTimeAsync(20000);
|
||||
|
||||
expect(wrapper.emitted('complete')).toBeUndefined();
|
||||
expect(wrapper.text()).not.toContain('Gmail');
|
||||
});
|
||||
|
||||
it('does not emit complete when never activated', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: false },
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20000);
|
||||
|
||||
expect(wrapper.emitted('complete')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders the publish button and flips it to Published after typing finishes', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
const button = wrapper.find('[data-testid="publish-agent-button"]');
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.text()).toBe('Publish');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(18000);
|
||||
expect(wrapper.find('[data-testid="publish-agent-button"]').text()).toBe('Published');
|
||||
});
|
||||
|
||||
it('plays the Telegram chat first, then the builder, and emits complete at the end', async () => {
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
// Chat phase: conversation is playing, builder sections untouched.
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(wrapper.text()).toContain('order #4712');
|
||||
expect(wrapper.text()).not.toContain('Claude Opus 5');
|
||||
expect(wrapper.emitted('complete')).toBeUndefined();
|
||||
|
||||
// Builder phase: sections are being filled in.
|
||||
await vi.advanceTimersByTimeAsync(14000);
|
||||
expect(wrapper.text()).toContain('Claude Opus 5');
|
||||
expect(wrapper.emitted('complete')).toBeUndefined();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
expect(wrapper.text()).toContain('Published');
|
||||
expect(wrapper.emitted('complete')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows the final state and completes promptly with reduced motion', async () => {
|
||||
setPrefersReducedMotion(true);
|
||||
const wrapper = mount(AgentBuilderVisualization, {
|
||||
props: { active: true },
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('Claude Opus 5');
|
||||
expect(text).toContain('Published');
|
||||
// The chat phase is skipped in the reduced-motion end state.
|
||||
expect(text).not.toContain('order #4712');
|
||||
expect(wrapper.emitted('complete')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,875 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, onUnmounted } from 'vue';
|
||||
import { N8nButton, N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
import {
|
||||
GMAIL_ICON_SVG,
|
||||
ANTHROPIC_ICON_SVG,
|
||||
GOOGLE_SHEETS_ICON_SVG,
|
||||
} from '../../workflows/process-invoices';
|
||||
import TelegramChatVisualization from './TelegramChatVisualization.vue';
|
||||
|
||||
const INSTRUCTIONS_TEXT =
|
||||
'You are a friendly support agent. Answer customer questions on Telegram, look up order details in Gmail and the FAQ sheet, and escalate to a human when unsure.';
|
||||
|
||||
const PANEL_APPEAR_MS = 150;
|
||||
const CHANNEL_ADDED_MS = 900;
|
||||
const TOOL_1_MS = 1550;
|
||||
const TOOL_2_MS = 1900;
|
||||
const SKILL_MS = 2350;
|
||||
const SCROLL_1_MS = 2900;
|
||||
const MODEL_MS = 3800;
|
||||
const SCROLL_2_MS = 4450;
|
||||
const TYPING_START_MS = 5000;
|
||||
const TYPING_INTERVAL_MS = 14;
|
||||
const CURSOR_APPEAR_MS = 200;
|
||||
const CURSOR_MOVE_START_MS = 350;
|
||||
const CURSOR_MOVE_DURATION_MS = 450;
|
||||
const CLICK_MS = CURSOR_MOVE_START_MS + CURSOR_MOVE_DURATION_MS + 50;
|
||||
const PUBLISH_PRESS_DURATION_MS = 180;
|
||||
const CURSOR_FADE_MS = CLICK_MS + PUBLISH_PRESS_DURATION_MS + 320;
|
||||
const COMPLETE_AFTER_PUBLISH_MS = CURSOR_FADE_MS + 600;
|
||||
const REDUCED_MOTION_COMPLETE_HOLD_MS = 800;
|
||||
const SPARKLE_DURATION_MS = 700;
|
||||
|
||||
const SPARKLE_DOTS = [
|
||||
{ x: '-26px', y: '-14px', delay: '0ms' },
|
||||
{ x: '-18px', y: '14px', delay: '60ms' },
|
||||
{ x: '-2px', y: '-20px', delay: '30ms' },
|
||||
{ x: '8px', y: '18px', delay: '80ms' },
|
||||
{ x: '22px', y: '-12px', delay: '20ms' },
|
||||
{ x: '28px', y: '8px', delay: '70ms' },
|
||||
];
|
||||
|
||||
const props = defineProps<{
|
||||
active: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const panelVisible = ref(false);
|
||||
const channelAdded = ref(false);
|
||||
const toolsShown = ref(0);
|
||||
const skillAdded = ref(false);
|
||||
const modelSelected = ref(false);
|
||||
const typedText = ref('');
|
||||
const typingDone = ref(false);
|
||||
const scrollY = ref(0);
|
||||
const publishPressed = ref(false);
|
||||
const published = ref(false);
|
||||
const sparklesVisible = ref(false);
|
||||
const chatActive = ref(false);
|
||||
const builderStarted = ref(false);
|
||||
const cursorVisible = ref(false);
|
||||
const cursorAtButton = ref(false);
|
||||
const cursorClicking = ref(false);
|
||||
|
||||
const viewportRef = ref<HTMLElement | null>(null);
|
||||
const contentRef = ref<HTMLElement | null>(null);
|
||||
const modelSectionRef = ref<HTMLElement | null>(null);
|
||||
const instructionsSectionRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const remainingChannels = computed(() =>
|
||||
channelAdded.value ? 'Slack, Linear' : 'Slack, Telegram, Linear',
|
||||
);
|
||||
|
||||
let timers: Array<ReturnType<typeof setTimeout>> = [];
|
||||
let typingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function clearTimers() {
|
||||
for (const t of timers) clearTimeout(t);
|
||||
timers = [];
|
||||
if (typingInterval) {
|
||||
clearInterval(typingInterval);
|
||||
typingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function schedule(ms: number, fn: () => void) {
|
||||
timers.push(setTimeout(fn, ms));
|
||||
}
|
||||
|
||||
function scrollToSection(el: HTMLElement | null, offset = 14) {
|
||||
if (!el || !contentRef.value || !viewportRef.value) return;
|
||||
const maxScroll = Math.max(0, contentRef.value.scrollHeight - viewportRef.value.clientHeight);
|
||||
scrollY.value = Math.min(Math.max(0, el.offsetTop - offset), maxScroll);
|
||||
}
|
||||
|
||||
function startTyping(onDone: () => void) {
|
||||
let index = 0;
|
||||
typingInterval = setInterval(() => {
|
||||
index++;
|
||||
typedText.value = INSTRUCTIONS_TEXT.slice(0, index);
|
||||
if (index >= INSTRUCTIONS_TEXT.length) {
|
||||
if (typingInterval) clearInterval(typingInterval);
|
||||
typingInterval = null;
|
||||
typingDone.value = true;
|
||||
onDone();
|
||||
}
|
||||
}, TYPING_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
panelVisible.value = false;
|
||||
channelAdded.value = false;
|
||||
toolsShown.value = 0;
|
||||
skillAdded.value = false;
|
||||
modelSelected.value = false;
|
||||
typedText.value = '';
|
||||
typingDone.value = false;
|
||||
scrollY.value = 0;
|
||||
publishPressed.value = false;
|
||||
published.value = false;
|
||||
sparklesVisible.value = false;
|
||||
chatActive.value = false;
|
||||
builderStarted.value = false;
|
||||
cursorVisible.value = false;
|
||||
cursorAtButton.value = false;
|
||||
cursorClicking.value = false;
|
||||
}
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.matchMedia === 'function' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
}
|
||||
|
||||
function showFinalState() {
|
||||
builderStarted.value = true;
|
||||
panelVisible.value = true;
|
||||
channelAdded.value = true;
|
||||
toolsShown.value = 2;
|
||||
skillAdded.value = true;
|
||||
modelSelected.value = true;
|
||||
typedText.value = INSTRUCTIONS_TEXT;
|
||||
typingDone.value = true;
|
||||
scrollToSection(instructionsSectionRef.value, 40);
|
||||
published.value = true;
|
||||
}
|
||||
|
||||
function runPublishSequence() {
|
||||
schedule(CURSOR_APPEAR_MS, () => (cursorVisible.value = true));
|
||||
schedule(CURSOR_MOVE_START_MS, () => (cursorAtButton.value = true));
|
||||
schedule(CLICK_MS, () => {
|
||||
cursorClicking.value = true;
|
||||
publishPressed.value = true;
|
||||
});
|
||||
schedule(CLICK_MS + PUBLISH_PRESS_DURATION_MS, () => {
|
||||
cursorClicking.value = false;
|
||||
publishPressed.value = false;
|
||||
published.value = true;
|
||||
sparklesVisible.value = true;
|
||||
});
|
||||
schedule(CLICK_MS + PUBLISH_PRESS_DURATION_MS + SPARKLE_DURATION_MS, () => {
|
||||
sparklesVisible.value = false;
|
||||
});
|
||||
schedule(CURSOR_FADE_MS, () => (cursorVisible.value = false));
|
||||
schedule(COMPLETE_AFTER_PUBLISH_MS, () => emit('complete'));
|
||||
}
|
||||
|
||||
function runBuilderAnimation() {
|
||||
schedule(PANEL_APPEAR_MS, () => (panelVisible.value = true));
|
||||
schedule(CHANNEL_ADDED_MS, () => (channelAdded.value = true));
|
||||
schedule(TOOL_1_MS, () => (toolsShown.value = 1));
|
||||
schedule(TOOL_2_MS, () => (toolsShown.value = 2));
|
||||
schedule(SKILL_MS, () => (skillAdded.value = true));
|
||||
schedule(SCROLL_1_MS, () => scrollToSection(modelSectionRef.value, 150));
|
||||
schedule(MODEL_MS, () => (modelSelected.value = true));
|
||||
schedule(SCROLL_2_MS, () => scrollToSection(instructionsSectionRef.value, 40));
|
||||
schedule(TYPING_START_MS, () => {
|
||||
startTyping(runPublishSequence);
|
||||
});
|
||||
}
|
||||
|
||||
function handleChatComplete() {
|
||||
builderStarted.value = true;
|
||||
runBuilderAnimation();
|
||||
}
|
||||
|
||||
function runAnimation() {
|
||||
resetState();
|
||||
|
||||
if (prefersReducedMotion()) {
|
||||
schedule(PANEL_APPEAR_MS, showFinalState);
|
||||
schedule(PANEL_APPEAR_MS + REDUCED_MOTION_COMPLETE_HOLD_MS, () => emit('complete'));
|
||||
return;
|
||||
}
|
||||
|
||||
chatActive.value = true;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(val) => {
|
||||
if (val) {
|
||||
runAnimation();
|
||||
} else {
|
||||
clearTimers();
|
||||
resetState();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(clearTimers);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.stage">
|
||||
<div :class="[$style.panel, panelVisible && $style.panelVisible]">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
:class="[$style.publishGroup, publishPressed && $style.publishPressed]"
|
||||
>
|
||||
<N8nButton
|
||||
:class="$style.groupButtonLeft"
|
||||
:disabled="published"
|
||||
variant="ghost"
|
||||
data-testid="publish-agent-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div :class="$style.publishFlex">
|
||||
<span v-if="published" :class="[$style.indicatorDot, $style.indicatorPublished]" />
|
||||
<span :class="{ [$style.indicatorPublishedText]: published }">
|
||||
{{ published ? 'Published' : 'Publish' }}
|
||||
</span>
|
||||
</div>
|
||||
</N8nButton>
|
||||
<N8nIconButton
|
||||
:class="$style.groupButtonRight"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
aria-label="Publish options"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<span v-if="sparklesVisible" :class="$style.sparkles">
|
||||
<span
|
||||
v-for="(dot, index) in SPARKLE_DOTS"
|
||||
:key="index"
|
||||
:class="$style.sparkleDot"
|
||||
:style="{ '--sx': dot.x, '--sy': dot.y, animationDelay: dot.delay }"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
$style.cursor,
|
||||
cursorVisible && $style.cursorVisible,
|
||||
cursorAtButton && $style.cursorAtButton,
|
||||
cursorClicking && $style.cursorClicking,
|
||||
]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M5.5 2.6v16.2l3.8-3.7 2.5 5.7 2.4-1-2.5-5.7h5.4L5.5 2.6Z"
|
||||
fill="#ffffff"
|
||||
stroke="#000000"
|
||||
stroke-width="1.3"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div ref="viewportRef" :class="$style.panelViewport">
|
||||
<div
|
||||
ref="contentRef"
|
||||
:class="$style.content"
|
||||
:style="{ transform: `translateY(-${scrollY}px)` }"
|
||||
>
|
||||
<div :class="$style.sections">
|
||||
<div :class="$style.channelsSection">
|
||||
<div :class="$style.sectionLabel">Channels</div>
|
||||
<div :class="$style.channelGrid">
|
||||
<div v-if="channelAdded" :class="[$style.channelCard, $style.popIn]">
|
||||
<N8nIcon icon="telegram" :size="20" :class="$style.channelIcon" />
|
||||
<div :class="$style.channelCardText">
|
||||
<span :class="$style.channelCardTitle">Telegram</span>
|
||||
<span :class="$style.channelCardSubtitle">Telegram account</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.channelCard">
|
||||
<N8nIcon icon="plus" :size="24" :class="$style.channelPlusIcon" />
|
||||
<div :class="$style.channelCardText">
|
||||
<span :class="$style.channelCardTitle">Add channel</span>
|
||||
<span :class="$style.channelCardSubtitle">{{ remainingChannels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.capabilityRow">
|
||||
<div :class="$style.rowLabel">Tools</div>
|
||||
<div :class="$style.chips">
|
||||
<template v-if="toolsShown > 0">
|
||||
<div :class="[$style.chip, $style.popIn]">
|
||||
<img :src="GMAIL_ICON_SVG" :class="$style.chipBrandIcon" alt="" />
|
||||
<span :class="$style.chipText">Gmail</span>
|
||||
</div>
|
||||
<div v-if="toolsShown > 1" :class="[$style.chip, $style.popIn]">
|
||||
<img :src="GOOGLE_SHEETS_ICON_SVG" :class="$style.chipBrandIcon" alt="" />
|
||||
<span :class="$style.chipText">Google Sheets</span>
|
||||
</div>
|
||||
<div :class="$style.plusButton">
|
||||
<N8nIcon icon="plus" :size="16" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :class="$style.emptyCta">Add tool</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.capabilityRow">
|
||||
<div :class="$style.rowLabel">Skills</div>
|
||||
<div :class="$style.chips">
|
||||
<template v-if="skillAdded">
|
||||
<div :class="[$style.chip, $style.popIn]">
|
||||
<N8nIcon icon="sparkles" :size="16" :class="$style.chipIcon" />
|
||||
<span :class="$style.chipText">Summarize conversations</span>
|
||||
</div>
|
||||
<div :class="$style.plusButton">
|
||||
<N8nIcon icon="plus" :size="16" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :class="$style.emptyCta">Add skill</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.capabilityRow">
|
||||
<div :class="$style.emptyCta">Add sub-agent</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.capabilityRow">
|
||||
<div :class="$style.rowLabel">Schedules</div>
|
||||
<div :class="$style.chips">
|
||||
<div :class="$style.emptyCta">Add schedule</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="modelSectionRef" :class="$style.fieldSection">
|
||||
<div :class="$style.sectionLabel">Model</div>
|
||||
<div :class="$style.modelTrigger">
|
||||
<div :class="$style.modelSelected">
|
||||
<template v-if="modelSelected">
|
||||
<img
|
||||
:src="ANTHROPIC_ICON_SVG"
|
||||
:class="[$style.modelBrandIcon, $style.popIn]"
|
||||
alt=""
|
||||
/>
|
||||
<span :class="[$style.modelName, $style.popIn]">Claude Opus 5</span>
|
||||
<span :class="[$style.modelCredential, $style.popIn]">Anthropic account</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<N8nIcon icon="bot" :size="16" :class="$style.modelPlaceholderIcon" />
|
||||
<span :class="$style.modelPlaceholder">Select model</span>
|
||||
</template>
|
||||
</div>
|
||||
<N8nIcon icon="chevron-down" :size="16" :class="$style.modelChevron" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="instructionsSectionRef" :class="$style.fieldSection">
|
||||
<div :class="$style.sectionLabel">Instructions</div>
|
||||
<div :class="$style.editor">
|
||||
<div :class="$style.editorToolbar">
|
||||
<div :class="$style.toolbarGroup">
|
||||
<N8nIcon icon="type" :size="14" />
|
||||
<N8nIcon icon="chevron-down" :size="12" />
|
||||
</div>
|
||||
<div :class="$style.toolbarDivider" />
|
||||
<N8nIcon icon="bold" :size="14" />
|
||||
<N8nIcon icon="italic" :size="14" />
|
||||
<N8nIcon icon="strikethrough" :size="14" />
|
||||
<div :class="$style.toolbarDivider" />
|
||||
<N8nIcon icon="list" :size="14" />
|
||||
<N8nIcon icon="list-checks" :size="14" />
|
||||
<N8nIcon icon="file-code" :size="14" />
|
||||
<N8nIcon icon="quote" :size="14" />
|
||||
<div :class="$style.toolbarDivider" />
|
||||
<N8nIcon icon="undo-2" :size="14" />
|
||||
<N8nIcon icon="redo-2" :size="14" />
|
||||
</div>
|
||||
<div :class="$style.editorContent">
|
||||
<span>{{ typedText }}</span>
|
||||
<span v-if="!typingDone" :class="$style.caret" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="[$style.chatSlot, builderStarted && $style.chatHidden]">
|
||||
<TelegramChatVisualization :active="chatActive" @complete="handleChatComplete" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.stage {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
> * {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
width: 660px;
|
||||
height: 384px;
|
||||
background: light-dark(var(--background--surface), var(--background--subtle));
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--lg);
|
||||
box-shadow: var(--shadow--md, var(--shadow--sm));
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.98);
|
||||
transition:
|
||||
opacity 0.35s ease,
|
||||
transform 0.35s ease;
|
||||
}
|
||||
|
||||
.panelVisible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.publishGroup {
|
||||
position: absolute;
|
||||
top: var(--spacing--sm);
|
||||
right: var(--spacing--sm);
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--3xs);
|
||||
background: light-dark(var(--background--surface), var(--background--subtle));
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.publishPressed {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sparkleDot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: -2.5px 0 0 -2.5px;
|
||||
border-radius: 50%;
|
||||
background-color: color-mix(in srgb, var(--background--brand) 68%, var(--text-color));
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--background--brand) 60%, transparent);
|
||||
opacity: 0;
|
||||
animation: agent-builder-sparkle 0.55s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes agent-builder-sparkle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, 0) scale(0.4);
|
||||
}
|
||||
|
||||
25% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--sx), var(--sy)) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transform: translate(555px, 95px);
|
||||
transform-origin: 4px 2px;
|
||||
transition:
|
||||
transform 0.45s cubic-bezier(0.3, 0.7, 0.25, 1),
|
||||
opacity 0.25s ease;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cursorVisible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cursorAtButton {
|
||||
transform: translate(583px, 26px);
|
||||
}
|
||||
|
||||
.cursorAtButton.cursorClicking {
|
||||
transform: translate(583px, 26px) scale(0.82);
|
||||
transition:
|
||||
transform 0.12s ease,
|
||||
opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.groupButtonLeft,
|
||||
.groupButtonLeft:disabled,
|
||||
.groupButtonLeft:hover:disabled {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.groupButtonRight {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: var(--border);
|
||||
}
|
||||
|
||||
.publishFlex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.indicatorDot {
|
||||
display: inline-block;
|
||||
width: var(--spacing--2xs);
|
||||
height: var(--spacing--2xs);
|
||||
margin-right: var(--spacing--2xs);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.indicatorPublished {
|
||||
background-color: var(--color--mint-600);
|
||||
}
|
||||
|
||||
.indicatorPublishedText {
|
||||
color: var(--color--text--tint-1);
|
||||
}
|
||||
|
||||
.chatSlot {
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.35s ease,
|
||||
transform 0.35s ease;
|
||||
}
|
||||
|
||||
.chatHidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.98);
|
||||
}
|
||||
|
||||
.panelViewport {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
black 14px,
|
||||
black calc(100% - 14px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing--lg) var(--spacing--xl);
|
||||
transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--md);
|
||||
}
|
||||
|
||||
.sectionLabel {
|
||||
line-height: var(--height--lg);
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--medium);
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.channelsSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.channelGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing--xs);
|
||||
}
|
||||
|
||||
.channelCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing--xs);
|
||||
border-radius: var(--radius--xs);
|
||||
gap: var(--spacing--xs);
|
||||
background-color: var(--background--surface);
|
||||
border: var(--border);
|
||||
box-shadow: var(--shadow--xs);
|
||||
}
|
||||
|
||||
.channelIcon {
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.channelPlusIcon {
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.channelCardText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing--3xs);
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channelCardTitle {
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.channelCardSubtitle {
|
||||
font-size: var(--font-size--xs);
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.capabilityRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing--2xs);
|
||||
min-height: var(--height--md);
|
||||
}
|
||||
|
||||
.rowLabel {
|
||||
flex: 0 0 max(11%, calc(var(--spacing--3xl) + var(--spacing--sm)));
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--medium);
|
||||
line-height: var(--height--md);
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
height: var(--height--md);
|
||||
padding: var(--spacing--xs);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--full);
|
||||
background: light-dark(var(--background--surface), var(--background--subtle));
|
||||
box-shadow: var(--shadow--xs);
|
||||
}
|
||||
|
||||
.chipIcon {
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chipBrandIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
transform: scale(1.3);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.chipText {
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--medium);
|
||||
white-space: nowrap;
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
padding-right: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.plusButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--height--md);
|
||||
height: var(--height--md);
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.emptyCta {
|
||||
font-size: var(--font-size--sm);
|
||||
line-height: var(--height--md);
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.fieldSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modelTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--height--lg);
|
||||
padding: 0 var(--spacing--xs);
|
||||
gap: var(--spacing--xs);
|
||||
border: var(--border);
|
||||
background-color: var(--background--surface);
|
||||
border-radius: var(--radius--2xs);
|
||||
font-size: var(--font-size--sm);
|
||||
}
|
||||
|
||||
.modelSelected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.modelBrandIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modelName {
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.modelCredential {
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.modelPlaceholderIcon {
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.modelPlaceholder {
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.modelChevron {
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor {
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--2xs);
|
||||
background-color: var(--background--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editorToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
padding: var(--spacing--2xs) var(--spacing--xs);
|
||||
border-bottom: var(--border);
|
||||
color: var(--text-color--subtler, var(--color--text--light));
|
||||
}
|
||||
|
||||
.toolbarGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.toolbarDivider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.editorContent {
|
||||
min-height: 96px;
|
||||
padding: var(--spacing--xs);
|
||||
font-size: var(--font-size--sm);
|
||||
line-height: var(--line-height--md);
|
||||
color: var(--text-color, var(--color--text--base));
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 1.5px;
|
||||
height: 1em;
|
||||
margin-left: 1px;
|
||||
vertical-align: text-bottom;
|
||||
background-color: var(--text-color, var(--color--text--base));
|
||||
animation: agent-builder-caret-blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.popIn {
|
||||
animation: agent-builder-pop-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
@keyframes agent-builder-pop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes agent-builder-caret-blink {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.panel,
|
||||
.content,
|
||||
.publishGroup,
|
||||
.cursor,
|
||||
.chatSlot {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.popIn {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.sparkleDot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch, onUnmounted } from 'vue';
|
||||
import { N8nIcon } from '@n8n/design-system';
|
||||
|
||||
interface ChatMessage {
|
||||
from: 'user' | 'agent';
|
||||
text: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const MESSAGES: ChatMessage[] = [
|
||||
{ from: 'user', text: 'Hi! Any update on my order #4712?', time: '9:41' },
|
||||
{
|
||||
from: 'agent',
|
||||
text: 'I found your receipt in Gmail - order #4712 shipped yesterday and arrives Thursday.',
|
||||
time: '9:41',
|
||||
},
|
||||
{ from: 'user', text: 'Can I still change the delivery address?', time: '9:42' },
|
||||
{
|
||||
from: 'agent',
|
||||
text: "Yes, until it leaves the warehouse. I've looped in a teammate to confirm the change.",
|
||||
time: '9:42',
|
||||
},
|
||||
];
|
||||
|
||||
const WINDOW_APPEAR_MS = 250;
|
||||
const FIRST_MESSAGE_MS = 700;
|
||||
const MESSAGE_GAP_MS = 1000;
|
||||
const AGENT_TYPING_MS = 1200;
|
||||
const COMPLETE_HOLD_MS = 1400;
|
||||
|
||||
const props = defineProps<{
|
||||
active: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const shownCount = ref(0);
|
||||
const agentTyping = ref(false);
|
||||
|
||||
let timers: Array<ReturnType<typeof setTimeout>> = [];
|
||||
|
||||
function clearTimers() {
|
||||
for (const t of timers) clearTimeout(t);
|
||||
timers = [];
|
||||
}
|
||||
|
||||
function schedule(ms: number, fn: () => void) {
|
||||
timers.push(setTimeout(fn, ms));
|
||||
}
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.matchMedia === 'function' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
visible.value = false;
|
||||
shownCount.value = 0;
|
||||
agentTyping.value = false;
|
||||
}
|
||||
|
||||
function runAnimation() {
|
||||
resetState();
|
||||
|
||||
if (prefersReducedMotion()) {
|
||||
schedule(WINDOW_APPEAR_MS, () => {
|
||||
visible.value = true;
|
||||
shownCount.value = MESSAGES.length;
|
||||
});
|
||||
schedule(WINDOW_APPEAR_MS + COMPLETE_HOLD_MS, () => emit('complete'));
|
||||
return;
|
||||
}
|
||||
|
||||
schedule(WINDOW_APPEAR_MS, () => (visible.value = true));
|
||||
|
||||
let at = WINDOW_APPEAR_MS + FIRST_MESSAGE_MS;
|
||||
MESSAGES.forEach((message, index) => {
|
||||
if (message.from === 'agent') {
|
||||
schedule(at, () => (agentTyping.value = true));
|
||||
at += AGENT_TYPING_MS;
|
||||
}
|
||||
schedule(at, () => {
|
||||
agentTyping.value = false;
|
||||
shownCount.value = index + 1;
|
||||
});
|
||||
at += MESSAGE_GAP_MS;
|
||||
});
|
||||
|
||||
schedule(at - MESSAGE_GAP_MS + COMPLETE_HOLD_MS, () => emit('complete'));
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(val) => {
|
||||
if (val) {
|
||||
runAnimation();
|
||||
} else {
|
||||
clearTimers();
|
||||
resetState();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(clearTimers);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[$style.window, visible && $style.windowVisible]">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.avatar">
|
||||
<N8nIcon icon="telegram" :size="42" />
|
||||
</div>
|
||||
<div :class="$style.headerText">
|
||||
<span :class="$style.headerName">Support Agent</span>
|
||||
<span :class="$style.headerStatus">bot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.messages">
|
||||
<template v-for="(message, index) in MESSAGES" :key="index">
|
||||
<div
|
||||
v-if="index < shownCount"
|
||||
:class="[$style.bubbleRow, message.from === 'user' && $style.bubbleRowOutgoing]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
$style.bubble,
|
||||
message.from === 'user' ? $style.bubbleOutgoing : $style.bubbleIncoming,
|
||||
]"
|
||||
>
|
||||
<p :class="$style.bubbleText">{{ message.text }}</p>
|
||||
<span :class="$style.bubbleMeta">
|
||||
{{ message.time }}
|
||||
<svg
|
||||
v-if="message.from === 'user'"
|
||||
:class="$style.readTicks"
|
||||
width="15"
|
||||
height="10"
|
||||
viewBox="0 0 18 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 6 L4.5 9.5 L10 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 6 L10 9.5 L15.5 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="agentTyping" :class="$style.bubbleRow">
|
||||
<div :class="[$style.bubble, $style.bubbleIncoming, $style.typingBubble]">
|
||||
<span :class="$style.typingDot" />
|
||||
<span :class="$style.typingDot" />
|
||||
<span :class="$style.typingDot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.inputBar">
|
||||
<div :class="$style.roundButton">
|
||||
<N8nIcon icon="paperclip" :size="18" />
|
||||
</div>
|
||||
<div :class="$style.messageInput">Message</div>
|
||||
<div :class="$style.roundButton">
|
||||
<N8nIcon icon="mic" :size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 460px;
|
||||
height: 400px;
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--lg);
|
||||
box-shadow: var(--shadow--md, var(--shadow--sm));
|
||||
overflow: hidden;
|
||||
background: light-dark(#e7ebf0, #0e1621);
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.98);
|
||||
transition:
|
||||
opacity 0.35s ease,
|
||||
transform 0.35s ease;
|
||||
}
|
||||
|
||||
.windowVisible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
padding: var(--spacing--xs) var(--spacing--md);
|
||||
background: light-dark(#fff, #17212b);
|
||||
border-bottom: 1px solid light-dark(#e3e6e8, #101921);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.headerText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.headerName {
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: light-dark(#000, #f5f5f5);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.headerStatus {
|
||||
font-size: var(--font-size--xs);
|
||||
color: light-dark(#787f85, #708499);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing--2xs);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: var(--spacing--sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bubbleRow {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubbleRowOutgoing {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 78%;
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
animation: telegram-bubble-pop 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
.bubbleIncoming {
|
||||
background: light-dark(#fff, #182533);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.bubbleOutgoing {
|
||||
background: light-dark(#effdde, #2b5278);
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bubbleText {
|
||||
margin: 0;
|
||||
font-size: var(--font-size--sm);
|
||||
line-height: 1.45;
|
||||
color: light-dark(#000, #f5f5f5);
|
||||
}
|
||||
|
||||
.bubbleMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 3px;
|
||||
margin-top: 1px;
|
||||
font-size: 10px;
|
||||
color: light-dark(#a0acb6, #708499);
|
||||
}
|
||||
|
||||
.bubbleOutgoing .bubbleMeta {
|
||||
color: light-dark(#4fae4e, #71baf5);
|
||||
}
|
||||
|
||||
.readTicks {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.typingBubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.typingDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: light-dark(#a0acb6, #708499);
|
||||
animation: telegram-typing-bounce 1.2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.inputBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--2xs) var(--spacing--xs);
|
||||
background: light-dark(#fff, #17212b);
|
||||
border-top: 1px solid light-dark(#e3e6e8, #101921);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.roundButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: light-dark(#f1f4f7, #242f3d);
|
||||
color: light-dark(#787f85, #708499);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 36px;
|
||||
padding: 0 var(--spacing--sm);
|
||||
border-radius: var(--radius--full);
|
||||
background: light-dark(#f1f4f7, #242f3d);
|
||||
font-size: var(--font-size--sm);
|
||||
color: light-dark(#a0acb6, #708499);
|
||||
}
|
||||
|
||||
@keyframes telegram-bubble-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes telegram-typing-bounce {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.window {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.typingDot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch, onUnmounted } from 'vue';
|
||||
|
||||
const APPEAR_DELAY_MS = 200;
|
||||
const BUBBLE_DELAY_MS = 350;
|
||||
const COMPLETE_DELAY_MS = BUBBLE_DELAY_MS + 700;
|
||||
|
||||
const WHATSAPP_ICON =
|
||||
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2260%22%20height%3D%2260%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22m4.868%2043.303%202.694-9.835a18.94%2018.94%200%200%201-2.535-9.489C5.032%2013.514%2013.548%205%2024.014%205a18.87%2018.87%200%200%201%2013.43%205.566A18.87%2018.87%200%200%201%2043%2023.994c-.004%2010.465-8.522%2018.98-18.986%2018.98h-.008a19%2019%200%200%201-9.073-2.311z%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M4.868%2043.803a.5.5%200%200%201-.482-.631l2.639-9.636a19.5%2019.5%200%200%201-2.497-9.556C4.532%2013.238%2013.273%204.5%2024.014%204.5a19.37%2019.37%200%200%201%2013.784%205.713A19.36%2019.36%200%200%201%2043.5%2023.994c-.004%2010.741-8.746%2019.48-19.486%2019.48a19.54%2019.54%200%200%201-9.144-2.277l-9.875%202.589a.5.5%200%200%201-.127.017%22/%3E%3Cpath%20fill%3D%22%23cfd8dc%22%20d%3D%22M24.014%205a18.87%2018.87%200%200%201%2013.43%205.566A18.87%2018.87%200%200%201%2043%2023.994c-.004%2010.465-8.522%2018.98-18.986%2018.98h-.008a19%2019%200%200%201-9.073-2.311l-10.065%202.64%202.694-9.835a18.94%2018.94%200%200%201-2.535-9.489C5.032%2013.514%2013.548%205%2024.014%205m0-1C12.998%204%204.032%2012.962%204.027%2023.979a20%2020%200%200%200%202.461%209.622L3.903%2043.04a.998.998%200%200%200%201.219%201.231l9.687-2.54a20%2020%200%200%200%209.197%202.244c11.024%200%2019.99-8.963%2019.995-19.98A19.86%2019.86%200%200%200%2038.153%209.86%2019.87%2019.87%200%200%200%2024.014%204%22/%3E%3Cpath%20fill%3D%22%2340c351%22%20d%3D%22M35.176%2012.832a15.67%2015.67%200%200%200-11.157-4.626c-8.704%200-15.783%207.076-15.787%2015.774a15.74%2015.74%200%200%200%202.413%208.396l.376.597-1.595%205.821%205.973-1.566.577.342a15.75%2015.75%200%200%200%208.032%202.199h.006c8.698%200%2015.777-7.077%2015.78-15.776a15.68%2015.68%200%200%200-4.618-11.161%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M19.268%2016.045c-.355-.79-.729-.806-1.068-.82-.277-.012-.593-.011-.909-.011s-.83.119-1.265.594-1.661%201.622-1.661%203.956%201.7%204.59%201.937%204.906%203.282%205.259%208.104%207.161c4.007%201.58%204.823%201.266%205.693%201.187s2.807-1.147%203.202-2.255.395-2.057.277-2.255c-.119-.198-.435-.316-.909-.554s-2.807-1.385-3.242-1.543-.751-.237-1.068.238c-.316.474-1.225%201.543-1.502%201.859s-.554.357-1.028.119-2.002-.738-3.815-2.354c-1.41-1.257-2.362-2.81-2.639-3.285-.277-.474-.03-.731.208-.968.213-.213.474-.554.712-.831.237-.277.316-.475.474-.791s.079-.594-.04-.831c-.117-.238-1.039-2.584-1.461-3.522%22/%3E%3C/svg%3E';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
active: boolean;
|
||||
sender?: string;
|
||||
message?: string;
|
||||
isOutgoing?: boolean;
|
||||
slideFrom?: 'left' | 'right';
|
||||
}>(),
|
||||
{
|
||||
sender: 'Customer',
|
||||
message: 'How do I reset my password?',
|
||||
isOutgoing: false,
|
||||
slideFrom: 'right',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const visible = ref(false);
|
||||
const bubbleVisible = ref(false);
|
||||
|
||||
let timers: Array<ReturnType<typeof setTimeout>> = [];
|
||||
|
||||
function clearTimers() {
|
||||
for (const t of timers) clearTimeout(t);
|
||||
timers = [];
|
||||
}
|
||||
|
||||
function runAnimation() {
|
||||
visible.value = false;
|
||||
bubbleVisible.value = false;
|
||||
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
}, APPEAR_DELAY_MS),
|
||||
);
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
bubbleVisible.value = true;
|
||||
}, APPEAR_DELAY_MS + BUBBLE_DELAY_MS),
|
||||
);
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
emit('complete');
|
||||
}, APPEAR_DELAY_MS + COMPLETE_DELAY_MS),
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(val) => {
|
||||
if (val) {
|
||||
runAnimation();
|
||||
} else {
|
||||
clearTimers();
|
||||
visible.value = false;
|
||||
bubbleVisible.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(clearTimers);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[$style.card, visible && $style.cardVisible, slideFrom === 'left' && $style.slideLeft]"
|
||||
>
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.avatar">
|
||||
<img :src="WHATSAPP_ICON" :class="$style.avatarIcon" alt="" />
|
||||
</div>
|
||||
<span :class="$style.sender">{{ sender }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="bubbleVisible" :class="[$style.bubbleRow, isOutgoing && $style.bubbleRowOutgoing]">
|
||||
<div :class="[$style.bubble, isOutgoing ? $style.bubbleOutgoing : $style.bubbleIncoming]">
|
||||
<p :class="$style.bubbleText">{{ message }}</p>
|
||||
<svg
|
||||
v-if="isOutgoing"
|
||||
:class="$style.readTicks"
|
||||
width="18"
|
||||
height="12"
|
||||
viewBox="0 0 18 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 6 L4.5 9.5 L10 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 6 L8.5 9.5 L14 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.card {
|
||||
width: 280px;
|
||||
padding: var(--spacing--sm) var(--spacing--md);
|
||||
background: var(--color--background--light-3);
|
||||
border-radius: var(--radius--lg);
|
||||
border: 1px solid
|
||||
light-dark(
|
||||
oklch(from var(--color--neutral-black) l c h / 0.08),
|
||||
oklch(from var(--color--neutral-white) l c h / 0.12)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--2xs);
|
||||
opacity: 0;
|
||||
transform: translateX(8px);
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
.slideLeft {
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
|
||||
.cardVisible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #25d366;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.avatarIcon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sender {
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--bold);
|
||||
color: var(--color--text--base);
|
||||
}
|
||||
|
||||
.bubbleRow {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubbleRowOutgoing {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 85%;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
animation: bubble-pop 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
.bubbleIncoming {
|
||||
background: light-dark(#ebebeb, #3a3a3a);
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.bubbleOutgoing {
|
||||
background: light-dark(#d9fdd3, #1a4a2a);
|
||||
border-bottom-right-radius: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bubbleText {
|
||||
margin: 0;
|
||||
font-size: var(--font-size--xs);
|
||||
color: var(--color--text--base);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.readTicks {
|
||||
color: #8696a0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes bubble-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,16 +29,6 @@ export const INSTANCE_AI_WORKFLOW_PREVIEW_SUGGESTIONS: readonly WorkflowPreviewS
|
|||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.processInvoices.prompt' as BaseTextKey,
|
||||
workflowFile: 'process-invoices',
|
||||
},
|
||||
{
|
||||
type: 'prompt',
|
||||
id: 'whatsapp-support',
|
||||
icon: 'message-circle' as IconName,
|
||||
labelKey:
|
||||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.whatsappSupport.label' as BaseTextKey,
|
||||
promptKey:
|
||||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.whatsappSupport.prompt' as BaseTextKey,
|
||||
workflowFile: 'whatsapp-support',
|
||||
},
|
||||
{
|
||||
type: 'prompt',
|
||||
id: 'schedule-social-posts',
|
||||
|
|
@ -49,4 +39,14 @@ export const INSTANCE_AI_WORKFLOW_PREVIEW_SUGGESTIONS: readonly WorkflowPreviewS
|
|||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.scheduleSocialPosts.prompt' as BaseTextKey,
|
||||
workflowFile: 'schedule-social-posts',
|
||||
},
|
||||
{
|
||||
type: 'prompt',
|
||||
id: 'telegram-agent',
|
||||
icon: 'bot' as IconName,
|
||||
labelKey:
|
||||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.telegramAgent.label' as BaseTextKey,
|
||||
promptKey:
|
||||
'experiments.instanceAiWorkflowPreviewSuggestions.suggestions.telegramAgent.prompt' as BaseTextKey,
|
||||
workflowFile: 'telegram-agent',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { PreviewWorkflow } from './types';
|
||||
import { scoreMyLeadsWorkflow } from './score-my-leads';
|
||||
import { processInvoicesWorkflow } from './process-invoices';
|
||||
import { whatsappSupportWorkflow } from './whatsapp-support';
|
||||
import { scheduleSocialPostsWorkflow } from './schedule-social-posts';
|
||||
import { telegramAgentWorkflow } from './telegram-agent';
|
||||
|
||||
export type {
|
||||
PreviewWorkflow,
|
||||
|
|
@ -18,8 +18,8 @@ export type {
|
|||
const workflowRegistry: Record<string, PreviewWorkflow> = {
|
||||
'score-my-leads': scoreMyLeadsWorkflow,
|
||||
'process-invoices': processInvoicesWorkflow,
|
||||
'whatsapp-support': whatsappSupportWorkflow,
|
||||
'schedule-social-posts': scheduleSocialPostsWorkflow,
|
||||
'telegram-agent': telegramAgentWorkflow,
|
||||
};
|
||||
|
||||
export function getPreviewWorkflow(workflowFile: string): PreviewWorkflow | undefined {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { PreviewWorkflow } from './types';
|
||||
|
||||
const GMAIL_ICON_SVG =
|
||||
export const GMAIL_ICON_SVG =
|
||||
'data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22256%22%20height=%22193%22%20preserveAspectRatio=%22xMidYMid%22%3E%3Cpath%20fill=%22%234285F4%22%20d=%22M58.182%20192.05V93.14L27.507%2065.077%200%2049.504v125.091c0%209.658%207.825%2017.455%2017.455%2017.455z%22/%3E%3Cpath%20fill=%22%2334A853%22%20d=%22M197.818%20192.05h40.727c9.659%200%2017.455-7.826%2017.455-17.455V49.505l-31.156%2017.837-27.026%2025.798z%22/%3E%3Cpath%20fill=%22%23EA4335%22%20d=%22m58.182%2093.14-4.174-38.647%204.174-36.989L128%2069.868l69.818-52.364%204.67%2034.992-4.67%2040.644L128%20145.504z%22/%3E%3Cpath%20fill=%22%23FBBC04%22%20d=%22M197.818%2017.504V93.14L256%2049.504V26.231c0-21.585-24.64-33.89-41.89-20.945z%22/%3E%3Cpath%20fill=%22%23C5221F%22%20d=%22m0%2049.504%2026.759%2020.07L58.182%2093.14V17.504L41.89%205.286C24.61-7.66%200%204.646%200%2026.23z%22/%3E%3C/svg%3E';
|
||||
|
||||
const ANTHROPIC_ICON_SVG =
|
||||
export const ANTHROPIC_ICON_SVG =
|
||||
'data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2246%22%20height=%2232%22%20fill=%22none%22%3E%3Cpath%20fill=%22%237D7D87%22%20d=%22M32.73%200h-6.945L38.45%2032h6.945zM12.665%200%200%2032h7.082l2.59-6.72h13.25l2.59%206.72h7.082L19.929%200zm-.702%2019.337%204.334-11.246%204.334%2011.246z%22/%3E%3C/svg%3E';
|
||||
|
||||
const GOOGLE_SHEETS_ICON_SVG =
|
||||
export const GOOGLE_SHEETS_ICON_SVG =
|
||||
'data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2260%22%20height=%2260%22%3E%3Cg%20fill=%22none%22%20fill-rule=%22evenodd%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20fill=%22%2328B446%22%20d=%22M35.69%201%2052%2017.225v39.087a3.67%203.67%200%200%201-1.084%202.61A3.7%203.7%200%200%201%2048.293%2060H12.707a3.7%203.7%200%200%201-2.623-1.078A3.67%203.67%200%200%201%209%2056.312V4.688a3.67%203.67%200%200%201%201.084-2.61A3.7%203.7%200%200%201%2012.707%201z%22/%3E%3Cpath%20fill=%22%236ACE7C%22%20d=%22M35.69%201%2052%2017.225H39.397c-2.054%200-3.707-1.829-3.707-3.872z%22/%3E%3Cpath%20fill=%22%23219B38%22%20d=%22M39.211%2017.225%2052%2022.48v-5.255z%22/%3E%3Cpath%20fill=%22%23FFF%22%20d=%22M20.12%2031.975c0-.817.662-1.475%201.483-1.475h17.794c.821%200%201.482.658%201.482%201.475v15.487c0%20.818-.661%201.475-1.482%201.475H21.603a1.476%201.476%200%200%201-1.482-1.474V31.974zm2.225%201.475h6.672v2.212h-6.672zm0%205.162h6.672v2.213h-6.672zm0%205.163h6.672v2.212h-6.672zm9.638-10.325h6.672v2.212h-6.672zm0%205.162h6.672v2.213h-6.672zm0%205.163h6.672v2.212h-6.672z%22/%3E%3Cpath%20fill=%22%2328B446%22%20d=%22M34.69%200%2051%2016.225v39.087a3.67%203.67%200%200%201-1.084%202.61A3.7%203.7%200%200%201%2047.293%2059H11.707a3.7%203.7%200%200%201-2.623-1.078A3.67%203.67%200%200%201%208%2055.312V3.688a3.67%203.67%200%200%201%201.084-2.61A3.7%203.7%200%200%201%2011.707%200z%22/%3E%3Cpath%20fill=%22%236ACE7C%22%20d=%22M34.69%200%2051%2016.225H38.397c-2.054%200-3.707-1.829-3.707-3.872z%22/%3E%3Cpath%20fill=%22%23219B38%22%20d=%22M38.211%2016.225%2051%2021.48v-5.255z%22/%3E%3Cpath%20fill=%22%23FFF%22%20d=%22M19.12%2030.975c0-.817.662-1.475%201.483-1.475h17.794c.821%200%201.482.658%201.482%201.475v15.487c0%20.818-.661%201.475-1.482%201.475H20.603a1.476%201.476%200%200%201-1.482-1.474V30.974zm2.225%201.475h6.672v2.212h-6.672zm0%205.162h6.672v2.213h-6.672zm0%205.163h6.672v2.212h-6.672zm9.638-10.325h6.672v2.212h-6.672zm0%205.162h6.672v2.213h-6.672zm0%205.163h6.672v2.212h-6.672z%22/%3E%3C/g%3E%3C/svg%3E';
|
||||
|
||||
const GOOGLE_CALENDAR_ICON_SVG =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import type { PreviewWorkflow } from './types';
|
||||
|
||||
export const telegramAgentWorkflow: PreviewWorkflow = {
|
||||
nodes: [],
|
||||
connections: [],
|
||||
fullCanvasVisualization: {
|
||||
type: 'agent-builder',
|
||||
},
|
||||
};
|
||||
|
|
@ -22,7 +22,7 @@ export type PreviewVisualizationType =
|
|||
| 'slack-message'
|
||||
| 'salesforce-card'
|
||||
| 'invoice-spreadsheet'
|
||||
| 'whatsapp-chat';
|
||||
| 'agent-builder';
|
||||
|
||||
export interface PreviewVisualization {
|
||||
type: PreviewVisualizationType;
|
||||
|
|
@ -51,5 +51,6 @@ export interface PreviewWorkflow {
|
|||
connections: PreviewWorkflowConnection[];
|
||||
inputVisualization?: PreviewVisualization;
|
||||
outputVisualization?: PreviewVisualization | PreviewOutputVisualization[];
|
||||
fullCanvasVisualization?: PreviewVisualization;
|
||||
crmCycle?: CrmCycleConfig;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -431,8 +431,9 @@ const CANVAS_NATURAL_HEIGHT_PX = 420;
|
|||
const PREVIEW_MIN_SCALE = 0.3;
|
||||
|
||||
const previewScale = ref(1);
|
||||
const previewRemainingSpace = ref(CANVAS_NATURAL_HEIGHT_PX);
|
||||
|
||||
useResizeObserver(emptyLayoutRef, () => {
|
||||
function updatePreviewScale() {
|
||||
if (!emptyLayoutRef.value || !centeredInputRef.value) return;
|
||||
const containerRect = emptyLayoutRef.value.getBoundingClientRect();
|
||||
const inputRect = centeredInputRef.value.getBoundingClientRect();
|
||||
|
|
@ -440,15 +441,20 @@ useResizeObserver(emptyLayoutRef, () => {
|
|||
const bottomPadding = parseFloat(layoutStyles.paddingBottom);
|
||||
const gap = parseFloat(layoutStyles.gap) || 0;
|
||||
const remainingSpace = containerRect.bottom - inputRect.bottom - bottomPadding - gap;
|
||||
previewRemainingSpace.value = Math.max(0, remainingSpace);
|
||||
previewScale.value = Math.max(0, Math.min(1, remainingSpace / CANVAS_NATURAL_HEIGHT_PX));
|
||||
});
|
||||
}
|
||||
|
||||
useResizeObserver(emptyLayoutRef, updatePreviewScale);
|
||||
useResizeObserver(centeredInputRef, updatePreviewScale);
|
||||
|
||||
const hasSpaceForPreview = computed(() => previewScale.value >= PREVIEW_MIN_SCALE);
|
||||
|
||||
const workflowPreviewWrapperStyle = computed(() => ({
|
||||
transform: `scale(${previewScale.value})`,
|
||||
transformOrigin: 'top center',
|
||||
height: `${CANVAS_NATURAL_HEIGHT_PX * previewScale.value}px`,
|
||||
height: `${previewRemainingSpace.value}px`,
|
||||
'--workflow-preview-canvas-height': `${Math.max(CANVAS_NATURAL_HEIGHT_PX, previewRemainingSpace.value)}px`,
|
||||
}));
|
||||
|
||||
useChatInputAutoFocus(chatInputRef, { disabled: isStartingThread });
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user