diff --git a/CHANGELOG.md b/CHANGELOG.md index 545ffc7735..f5d721bed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to ### Added +- Slash-command skills (`/design`, `/diagnose`, `/qa`) in the global AI + assistant, and the assistant's conversation history is now stored as Apollo + returns it. Needs the matching Apollo release first. + [#5236](https://github.com/OpenFn/lightning/pull/5236) - The AI assistant now shows a warning banner above its chat input when the current workflow is live. [#5205](https://github.com/OpenFn/lightning/pull/5205) diff --git a/assets/js/collaborative-editor/CollaborativeEditor.tsx b/assets/js/collaborative-editor/CollaborativeEditor.tsx index e54124926b..3d86aed819 100644 --- a/assets/js/collaborative-editor/CollaborativeEditor.tsx +++ b/assets/js/collaborative-editor/CollaborativeEditor.tsx @@ -36,6 +36,7 @@ import { StoreProvider } from './contexts/StoreProvider'; import { useActionLock } from './hooks/useActionLock'; import { useHistoryCommands } from './hooks/useHistory'; import { + useAISkills, useIsNewWorkflow, useLatestSnapshotLockVersion, useLimits, @@ -366,6 +367,7 @@ function LandingScreenWrapper({ const showLandingScreen = useShowLandingScreen(); const aiLimit = useLimits().ai_assistant; const aiLimitReached = aiLimit != null && !aiLimit.allowed; + const skills = useAISkills(); const { openYAMLImportModal, openTemplateBrowserModal, @@ -399,6 +401,7 @@ function LandingScreenWrapper({ isBuildingFromScratch={isBuildingFromScratch} onBrowseTemplates={openTemplateBrowserModal} onImportYAML={openYAMLImportModal} + skills={skills} /> diff --git a/assets/js/collaborative-editor/components/AIAssistantPanel.tsx b/assets/js/collaborative-editor/components/AIAssistantPanel.tsx index 383ea29684..209ee4831e 100644 --- a/assets/js/collaborative-editor/components/AIAssistantPanel.tsx +++ b/assets/js/collaborative-editor/components/AIAssistantPanel.tsx @@ -15,6 +15,7 @@ import { useSelectedRunId } from '../hooks/useHistory'; import { useContentLocked, useIsNewWorkflow } from '../hooks/useSessionContext'; import { describeLifecycleError } from '../lib/errors'; import { notifications } from '../lib/notifications'; +import type { AISkill } from '../types/sessionContext'; import { AlertDialog } from './AlertDialog'; import { ChatInput } from './ChatInput'; @@ -61,6 +62,8 @@ interface AIAssistantPanelProps { * Switch the current workflow to draft, for the live workflow notice */ switchToDraft?: () => Promise; + /** Skills a leading slash command can invoke */ + skills?: AISkill[]; } interface MessageOptions { @@ -100,6 +103,7 @@ export function AIAssistantPanel({ connectionState = 'connected', aiLimit = null, switchToDraft = () => Promise.resolve(undefined), + skills, }: AIAssistantPanelProps) { const [view, setView] = useState<'chat' | 'sessions'>( sessionId ? 'chat' : 'sessions' @@ -477,6 +481,7 @@ export function AIAssistantPanel({ placeholder={placeholderText} disabledMessage={disabledMessage} selectedRunId={selectedRunId} + skills={skills} /> {/* About AI Assistant Modal */} diff --git a/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx b/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx index 700e2fb767..3d3f4d618f 100644 --- a/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx +++ b/assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; const INITIAL_LOADING_STATUSES = [ - 'Thinking about the question...', + 'Thinking...', 'Working on it...', 'Processing your request...', 'Examining your question...', @@ -54,6 +54,7 @@ import { useSession, } from '../hooks/useSession'; import { + useAISkills, useIsNewWorkflow, useLimits, useProject, @@ -185,6 +186,7 @@ export function AIAssistantPanelWrapper({ const user = useUser(); const workflow = useWorkflowState(state => state.workflow); const limits = useLimits(); + const skills = useAISkills(); // AI can apply changes if: not readonly OR is a new workflow (being created) const canApplyChanges = !isReadOnly || isNewWorkflow; @@ -802,6 +804,7 @@ export function AIAssistantPanelWrapper({ connectionState={sessionId ? connectionState : 'connected'} aiLimit={limits.ai_assistant ?? null} switchToDraft={switchToDraft} + skills={skills} > MAX_MESSAGE_LENGTH; - const showCount = input.length >= COUNT_FROM; + const textareaRef = useRef(null); + const slash = useSlashCommand(skills, textareaRef); + const { input, skill, content } = slash; + + const tooLong = content.length > MAX_MESSAGE_LENGTH; + const showCount = content.length >= COUNT_FROM; + const canSend = !!content && !isLoading && !isDisabled && !tooLong; const [attachLogs, setAttachLogs] = useState(() => { if (!storageKey) { @@ -103,7 +114,6 @@ export function ChatInput({ } }); - const textareaRef = useRef(null); const isLoadingFromStorageRef = useRef(false); const attachmentState: Record = { @@ -204,7 +214,7 @@ export function ChatInput({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (!input.trim() || isLoading || isDisabled || tooLong) return; + if (!canSend) return; const options: MessageOptions = {}; // The run rides along so what we promise to attach and what the backend @@ -215,11 +225,13 @@ export function ChatInput({ options.follow_run_id = selectedRunId; } - onSendMessage?.(input.trim(), options); - setInput(''); + onSendMessage?.(content, options); + slash.reset(); }; - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (slash.handleKeyDown(e)) return; + if ( e.key === 'Enter' && !e.shiftKey && @@ -241,11 +253,12 @@ export function ChatInput({ side="top" >
+
)} -