@chotu-cursor/angular
v0.0.1-alpha.14
Published
A configurable, animated assistant cursor with optional AI responses and intent-based navigation.
Readme
Chotu Cursor (Angular)
A configurable, animated assistant cursor with optional AI responses and intent-based navigation.
Install
npm i @chotu-cursor/angularUse the component
Import (standalone)
import { Component } from '@angular/core';
import { ChotuCursor } from '@chotu-cursor/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [ChotuCursor],
template: `<chotu-cursor [config]="config" />`
})
export class AppComponent {
config = { theme: 'auto' };
}Basic config
<chotu-cursor [config]="{
theme: 'dark',
pointer: { size: { width: 28, height: 28 }, direction: 'right' },
navigation: { links: { pricing: '/pricing', contact: '/contact' } },
chat: { maxVisibleActions: 8, scrollableList: true }
}"/>Actions as functions (custom behavior)
// component.ts
config = {
navigation: {
links: {
pricing: '/pricing',
'alert-demo': ({ title }) => {
alert(`Custom action: ${title}`);
return false; // prevent default navigation
},
contact: {
url: '/contact?from=cursor',
onClick: ({ navigate }) => navigate() // do custom work, then navigate
}
}
}
};Prompt the user (free-text and nested choices)
You can ask the user for input inside any action handler using the ActionContext helpers.
config = {
navigation: {
links: {
'book demo': async ({ waitForUserInput, messageFromChotu }) => {
// Free-text prompt (user types in the floating input). While typing, action suggestions are hidden.
const email = await waitForUserInput('Tell me your email');
if (!email) return false;
// Nested choice prompt (uses the action list as choices). Cursor says the prompt until selection.
const time = await waitForUserInput('What time works for you?', {
morning: ['8:00AM'],
evening: ['10:00PM']
});
if (!time) return false;
// Speak a confirmation and temporarily hide the action list until it finishes
messageFromChotu?.('Booked Demo');
return false; // skip default navigation
}
}
}
};Behavior notes:
- Free-text prompt: the input field is focused and the action list is hidden while typing.
- Choice prompt: the prompt is shown in “talking” state and the action list renders the provided options. Selecting a leaf value resolves the promise.
messageFromChotu(text, durationMs?): shows a transient message and hides the default action list until it completes.
With AI (remote provider - recommended)
<chotu-cursor [config]="{
ai: {
provider: 'remote',
model: 'gemini-2.5-flash-lite',
serverUrl: 'https://your-backend.com/api/ai/generate',
headers: { Authorization: 'Bearer <token>' },
roleContext: 'You help users navigate this site.',
systemInstructions: 'Use only links passed from the site.'
},
navigation: { links: { docs: '/docs', support: '/support' } }
}"/>With AI (google provider - dev only)
<chotu-cursor [config]="{
ai: {
provider: 'google',
model: 'gemini-2.5-flash-lite',
apiKey: 'YOUR_KEY',
allowClientKeyInProd: false
}
}"/>Configuration overview (CursorConfig)
All options are optional unless stated. Defaults are in parentheses.
Root
- state: 'docked' | 'active' ('docked')
- theme: 'auto' | 'light' | 'dark' ('auto')
- exposeApisGlobally: boolean (false)
pointer
- size: { width: number; height: number } ({28, 28})
- color: string ('#000')
- direction: 'right' | 'left' ('right')
- customSvg?: string (raw SVG)
- customImage?: string (url or data URL)
chat
- state: string ('active')
- dockedMessage: string
- thinkingMessage: string
- activeMessage: string
- messageWhileWaitingForAI: string
- labels.exitButton: string
- maxVisibleActions?: number (3)
- scrollableList?: boolean (false)
userInput
- enabled: boolean (true)
- placeholder: string
- maxLength: number (500)
- labels: navigateHint, sendHintFocused, sendHintBlurred, selectedTextLabel, enterAriaPrefix
ai
Choose one provider. If omitted/misconfigured, AI is disabled and intent-based navigation is used.
Shared:
- model: string ('gemini-2.5-flash-lite')
- initialContext: string ('')
- roleContext: string (defaults to guidance text)
- systemInstructions: string ('')
Provider 'google' (client SDK):
- provider: 'google'
- apiKey: string ('')
- allowClientKeyInProd: boolean (false)
Provider 'remote' (recommended):
- provider: 'remote'
- serverUrl: string (required)
- headers?: Record<string, string>
position
- startPosition: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' ('bottom-left')
- offsetX: number (24)
- offsetY: number (24)
behavior
- userTextSelectionMode: boolean (true)
- autoNavigate: boolean (true)
- restoreToCenterAfterNavigating: boolean (false)
- exitCommands: string[] (['exit','quit','close','bye','goodbye','dismiss','hide','dock','go away','leave','stop'])
- inactivityTimeout: number ms (30000)
- activationHotKey: string ('/') — global hotkey to activate; set '' to disable
- positionOnHotKeyActivation: 'to-user' | 'to-center' ('to-center')
- cursorMoveKeyBinding: KeyBinding — configure the key combination to move cursor (default: click-only)
- key?: 'Shift' | 'Control' | 'Alt' | 'Meta' | string | null (
null) — set tonullfor click-only mode - requireClick: boolean (true) — if false, just pressing the key moves the cursor (ignored in click-only mode)
- key?: 'Shift' | 'Control' | 'Alt' | 'Meta' | string | null (
navigation
- links: Record<string, string | Function | { url?: string; onClick?: Function }> ({})
- preserveHistory: boolean (true)
- preserveQueryParams: boolean (true)
Link types and callback behavior
- Normal URL:
'pricing': '/pricing'→ navigates, no callback. - Pure JS Function:
'alert-demo': ({ navigate }) => { /* side-effects */ navigate('/path'); return false; }→ your function runs; callnavigate(...)yourself; returnfalseto skip any default. - URL with JS Function:
'checkout': { url: '/checkout', onClick: ({ navigate }) => { /* side-effects */ navigate(); } }→ callback runs first; returningfalsecancels navigation; otherwise it navigates tourl.
Global API (optional)
Set exposeApisGlobally: true to expose a safe proxy at window.chotuCursor:
- Methods:
activate(delay?, toCentre?),dock(),toggleDock(),isDocked(),sendMessage(msg),bringToUser(),state(),waitForUserInput(prompt: string, options?: Record<string, any>),messageFromChotu(text: string, durationMs?: number),updateAiContext(newContext: string)— dynamically update AI's initial context.
Peer dependencies
@angular/common^20.x@angular/core^20.x@google/genai^1.19.x (only when using provider 'google')
Styling & theming
- The host sets
data-theme="auto|light|dark". Use it to scope styles if needed. - You can override pointer color via CSS variable
--pointer-default-color(fallback#625DF5).
Example in src/styles.scss:
:root {
--pointer-default-color: #7c3aed; // violet
}
[data-theme='dark'] {
--pointer-default-color: #60a5fa; // light blue in dark mode
}Router notes
- The library works with or without Angular Router. If Router isn’t provided, state persistence falls back to manual/session mode.
Custom pointer
const svg = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#FF0080"/>
</svg>`;
<chotu-cursor [config]="{
pointer: { size: { width: 32, height: 32 }, customSvg: svg, direction: 'right' }
}"/>Custom key binding for cursor movement
// Default: click-only mode (cursor follows any click, ignores interactive elements)
// No configuration needed - this is the default behavior!
// Or use Shift+Click if you prefer a modifier key
<chotu-cursor [config]="{
behavior: {
cursorMoveKeyBinding: {
key: 'Shift',
requireClick: true
}
}
}"/>
// Or use just Alt key without clicking
<chotu-cursor [config]="{
behavior: {
cursorMoveKeyBinding: {
key: 'Alt',
requireClick: false
}
}
}"/>Remote backend contract
- Request JSON:
{ prompt: string, model: string, initialContext?: string }(the library may also sendroleContext,systemInstructions,links) - Response JSON:
{ "ok": true, "text": "{\"isSuccess\":true,\"message\":\"...\",\"actionTree\":[{\"title\":\"...\",\"url\":\"...\"}]}" }Error:
{ "ok": false, "error": "AI relay failed: <reason>" }