use triggerable by ai compoennt

This commit is contained in:
centdix
2025-05-26 15:48:29 +02:00
parent c2f40cb921
commit 7cb8f0afed
5 changed files with 108 additions and 69 deletions

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { triggerablesByAI } from '$lib/stores'
let { id, description, onTrigger, children } = $props<{
id: string
description: string
onTrigger?: (id: string) => void
children?: () => any
}>()
// Use $effect for lifecycle management
// The cleanup function returned from $effect will run on unmount
$effect(() => {
// Register this component
triggerablesByAI.update((triggers) => {
return { ...triggers, [id]: { description, onTrigger } }
})
// Return cleanup function that will run on unmount
return () => {
triggerablesByAI.update((triggers) => {
const newTriggers = { ...triggers }
delete newTriggers[id]
return newTriggers
})
}
})
</script>
{@render children?.()}

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import TriggerableByAI from './TriggerableByAI.svelte'
let isVisible = $state(true)
function toggleVisibility() {
isVisible = !isVisible
}
</script>
{#if isVisible}
<TriggerableByAI
id="example-component"
description="This is an example component that can be triggered by AI"
onTrigger={(id: string) => {
console.log('Triggering example component', id)
}}
>
<div class="p-4 border rounded">
<!-- Your actual component content goes here -->
<h3 class="text-lg font-medium">AI Triggerable Component</h3>
<p
>This component will register itself with the AI system on mount and unregister on unmount.</p
>
<button
class="mt-2 px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
onclick={toggleVisibility}
>
Hide Component
</button>
</div>
</TriggerableByAI>
{:else}
<button
class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
onclick={toggleVisibility}
>
Show Component
</button>
{/if}

View File

@@ -6,7 +6,7 @@ import type {
ChatCompletionMessageToolCall,
ChatCompletionTool
} from 'openai/resources/index.mjs'
import { userStore } from '$lib/stores'
import { triggerablesByAI } from '$lib/stores'
// System prompt for the LLM
export const CHAT_SYSTEM_PROMPT = `
@@ -43,25 +43,16 @@ const EXECUTE_COMMAND_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: 'execute_command',
description: 'Executes a command on the page, such as clicking a button',
description: 'Executes a command to trigger an AI component',
parameters: {
type: 'object',
properties: {
type: {
id: {
type: 'string',
description: 'The type of command (click, input, select, etc.)',
enum: ['click', 'input', 'select']
},
selector: {
type: 'string',
description: 'CSS selector or XPath to identify the element'
},
value: {
type: 'string',
description: 'Value to use for input or select commands (optional)'
description: 'ID of the AI-triggerable component'
}
},
required: ['type', 'selector']
required: ['id']
}
}
}
@@ -91,77 +82,49 @@ ${html.substring(0, 15000)}${html.length > 15000 ? '...(truncated)' : ''}
// Helper function to extract interactive elements from the page
function extractInteractiveElements(): string {
try {
const elements = document.querySelectorAll('button, a, input, select, [role="button"]')
let result = 'INTERACTIVE_ELEMENTS:\n'
// Get components registered in the triggerablesByAI store
const registeredComponents = get(triggerablesByAI)
let result = 'TRIGGERABLE_COMPONENTS:\n'
elements.forEach((el, index) => {
const tagName = el.tagName.toLowerCase()
const text = el.textContent?.trim() || ''
const id = el.id ? `id="${el.id}"` : ''
const classes = Array.from(el.classList).join(' ')
const classAttr = classes ? `class="${classes}"` : ''
const type = el.getAttribute('type') || ''
const typeAttr = type ? `type="${type}"` : ''
const value = (el as HTMLInputElement).value || ''
const valueAttr = value ? `value="${value}"` : ''
const placeholder = el.getAttribute('placeholder') || ''
const placeholderAttr = placeholder ? `placeholder="${placeholder}"` : ''
const href = (el as HTMLAnchorElement).href || ''
const hrefAttr = href ? `href="${href}"` : ''
const role = el.getAttribute('role') || ''
const roleAttr = role ? `role="${role}"` : ''
// If there are no components registered, return a message
if (Object.keys(registeredComponents).length === 0) {
return 'No AI-triggerable components are currently available on this page.\n'
}
// Create a basic selector for this element
const selector = el.id
? `#${el.id}`
: tagName + (classes ? `.${classes.replace(/\s+/g, '.')}` : '')
result += `[${index}] <${tagName} ${id} ${classAttr} ${typeAttr} ${valueAttr} ${placeholderAttr} ${hrefAttr} ${roleAttr}>${text}</${tagName}> (selector: ${selector})\n`
// List each registered component with its ID and description
Object.entries(registeredComponents).forEach(([id, component], index) => {
result += `[${index}] ID: "${id}" - ${component.description}\n`
})
return result
} catch (error) {
console.error('Error extracting interactive elements:', error)
return 'Error extracting interactive elements: ' + error.message
console.error('Error extracting triggerable components:', error)
return 'Error extracting triggerable components: ' + error.message
}
}
// Function to execute commands on the page
function executeCommand(args: any): string {
const { type, selector, value } = args
function executeCommand(args: { id: string }): string {
const { id } = args
try {
const element = document.querySelector(selector)
if (!element) {
return `No element found matching selector: ${selector}`
// Handle triggering AI components
if (!id) {
return 'Trigger command requires an id parameter'
}
switch (type) {
case 'click':
;(element as HTMLElement).click()
return `Successfully clicked element: ${selector}`
const components = get(triggerablesByAI)
const component = components[id]
case 'input':
if (!value) {
return 'Input command requires a value'
}
const inputElement = element as HTMLInputElement
inputElement.value = value
inputElement.dispatchEvent(new Event('input', { bubbles: true }))
inputElement.dispatchEvent(new Event('change', { bubbles: true }))
return `Successfully set input value for element: ${selector}`
if (!component) {
return `No triggerable component found with id: ${id}`
}
case 'select':
if (!value) {
return 'Select command requires a value'
}
const selectElement = element as HTMLSelectElement
selectElement.value = value
selectElement.dispatchEvent(new Event('change', { bubbles: true }))
return `Successfully set select value for element: ${selector}`
default:
return `Unsupported command type: ${type}`
if (component.onTrigger) {
component.onTrigger(id)
return `Successfully triggered component: ${id} (${component.description})`
} else {
return `Component ${id} has no trigger handler defined`
}
} catch (error) {
console.error('Error executing command:', error)

View File

@@ -170,6 +170,10 @@ export const copilotSessionModel = writable<AIProviderModel | undefined>(
)
export const usedTriggerKinds = writable<string[]>([])
export const triggerablesByAI = writable<
Record<string, { description: string; onTrigger: (id: string) => void }>
>({})
type SQLBaseSchema = {
[schemaKey: string]: {
[tableKey: string]: {

View File

@@ -55,6 +55,7 @@
import { Menubar } from '$lib/components/meltComponents'
import GlobalChatDrawer from '$lib/components/chat/GlobalChatDrawer.svelte'
import Input from '$lib/components/globalchat/Input.svelte'
import TriggerableByAIExample from '$lib/components/TriggerableByAIExample.svelte'
OpenAPI.WITH_CREDENTIALS = true
let menuOpen = false
@@ -662,6 +663,7 @@
<main class="min-h-screen">
<div class="relative w-full h-full">
<Input />
<TriggerableByAIExample />
<div
class={classNames(