@tangleai/assistant 0.21.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/component.js CHANGED
@@ -1,4 +1,3 @@
1
- //@ts-check
2
1
  /** The visual assistant owns one app, its request lifecycle and its DOM listeners. */
3
2
  import { createApp, formEventFields } from '@jarenjs/app';
4
3
  import { createMdComponent } from '@jarenjs/md/component';
@@ -6,88 +5,108 @@ import { highlightPlugin } from '@jarenjs/md/plugins';
6
5
  import { mermaidPlugin } from '@jarenjs/mermaid/plugin';
7
6
  import { createLedger } from '@tangleai/context';
8
7
  import { createToolbox } from '@tangleai/agents';
9
- import { createAssistantState, createAssistantController, ASSISTANT_ACTIONS } from './index.js';
10
- import { createAssistantRules } from './views.js';
11
- import { assistantView } from './viewmodel.js';
12
-
13
- /**
14
- * @typedef {{ title?: string, launchLabel?: string, launchTitle?: string,
15
- * intro?: string, capabilities?: string[], settingsHint?: string }} AssistantPresentation
16
- * @typedef {AssistantPresentation & Partial<Omit<Parameters<typeof createAssistantController>[0], 'getApp'>> & {
17
- * settings?: any, transcript?: any, open?: boolean,
18
- * schedule?: (flush: () => void) => void, onError?: (error: Error) => void,
19
- * renderMarkdown?: (source: string) => any }} AssistantOptions
20
- */
8
+ import { createAssistantState, createAssistantController, ASSISTANT_ACTIONS } from "./index.js";
9
+ import { createAssistantRules } from "./views.js";
10
+ import { assistantView } from "./viewmodel.js";
21
11
  const DEFAULT_PRESENTATION = {
22
- title: 'Assistant', launchLabel: ' Assistant', launchTitle: 'Open assistant',
23
- intro: 'Describe what you want to get done. The assistant can use the tools this host provides.',
24
- capabilities: [],
25
- settingsHint: 'Provider requests use your settings. This host controls local persistence.',
12
+ title: 'Assistant', launchLabel: ' Assistant', launchTitle: 'Open assistant',
13
+ intro: 'Describe what you want to get done. The assistant can use the tools this host provides.',
14
+ capabilities: [],
15
+ settingsHint: 'Provider requests use your settings. This host controls local persistence.',
26
16
  };
27
17
  let nextInstance = 0;
28
- /** @param {AssistantPresentation} value */
18
+ /** @param value */
29
19
  function presentation(value) {
30
- return Object.fromEntries(Object.entries(value).filter(([key]) => Object.hasOwn(DEFAULT_PRESENTATION, key)));
20
+ return Object.fromEntries(Object.entries(value).filter(([key]) => Object.hasOwn(DEFAULT_PRESENTATION, key)));
31
21
  }
32
- /** @param {HTMLElement | null} node @param {AssistantOptions} [options] */
22
+ /** @param node @param [options] */
33
23
  export function mountAssistant(node, options = {}) {
34
- let app, disposed = false, closing;
35
- const id = `tangle-assistant-${++nextInstance}`;
36
- let settings = options.settings ?? null, transcript = options.transcript ?? null;
37
- const aiStorage = options.aiStorage ?? { read: () => settings, write: value => { settings = structuredClone(value); } };
38
- const aiChat = options.aiChat ?? { read: () => transcript, write: value => { transcript = structuredClone(value); } };
39
- const md = createMdComponent({ plugins: [highlightPlugin(), mermaidPlugin({ theme: 'host' })], headingIds: true, headingAnchors: true });
40
- const renderMarkdown = options.renderMarkdown ?? (source => md.view(source, { slugPrefix: `user-content-${id}-` }));
41
- const controller = createAssistantController({ ...options, aiStorage, aiChat,
42
- ledger: options.ledger ?? createLedger(), toolbox: options.toolbox ?? createToolbox(), getApp: () => app });
43
- app = createApp({
44
- state: { ai: { ...createAssistantState(aiStorage.read(), aiChat.read()), open: options.open ?? false }, presentation: { ...DEFAULT_PRESENTATION, ...presentation(options) } },
45
- actions: { ...ASSISTANT_ACTIONS, 'assistant/presentation': { patch: [{ op: 'replace', path: '/presentation', value: '$payload' }] } },
46
- view: { $jslt: '0.1', modes: { assistant: { unmatched: 'error' } }, rules: [
47
- { match: '$', body: ['section', { class: 'tangle-assistant', 'data-assistant-instance': id }, { $apply: ['$.ui.assistant', 'assistant'] }] },
48
- ...createAssistantRules(`${id}-models`),
49
- ] },
50
- }, {
51
- node, document: node?.ownerDocument, schedule: options.schedule, onError: options.onError,
52
- eventFields: formEventFields(), effects: controller.effects,
53
- afterRender: () => { if (node?.querySelectorAll) md.hydrate(node); },
54
- viewModel: state => ({ ui: { assistant: { ...assistantView(state.ai, renderMarkdown), ...state.presentation,
55
- capabilities: state.presentation.capabilities.map(text => ({ text })) } } }),
56
- });
57
- // A Markdown fragment belongs to this component, not to its host's hash router.
58
- function click(event) {
59
- const anchor = event.target.closest?.('a[href^="#"]');
60
- const href = anchor?.getAttribute('href');
61
- if (!href || href.startsWith('#/')) return;
62
- event.preventDefault();
63
- let fragment; try { fragment = decodeURIComponent(href.slice(1)); } catch { return; }
64
- for (const target of node?.querySelectorAll('[id]') ?? []) {
65
- if (target.id === fragment) { target.scrollIntoView?.({ block: 'nearest' }); break; }
24
+ let app, disposed = false, closing;
25
+ const id = `tangle-assistant-${++nextInstance}`;
26
+ let settings = options.settings ?? null, transcript = options.transcript ?? null;
27
+ const aiStorage = options.aiStorage ?? { read: () => settings, write: (value) => { settings = structuredClone(value); } };
28
+ const aiChat = options.aiChat ?? { read: () => transcript, write: (value) => { transcript = structuredClone(value); } };
29
+ const md = createMdComponent({ plugins: [highlightPlugin(), mermaidPlugin({ theme: 'host' })], headingIds: true, headingAnchors: true });
30
+ const renderMarkdown = options.renderMarkdown ?? (source => md.view(source, { slugPrefix: `user-content-${id}-` }));
31
+ const controller = createAssistantController({
32
+ ...options, aiStorage, aiChat,
33
+ ledger: options.ledger ?? createLedger(), toolbox: options.toolbox ?? createToolbox(), getApp: () => app
34
+ });
35
+ app = createApp({
36
+ state: { ai: { ...createAssistantState(aiStorage.read(), aiChat.read()), open: options.open ?? false }, presentation: { ...DEFAULT_PRESENTATION, ...presentation(options) } },
37
+ actions: { ...ASSISTANT_ACTIONS, 'assistant/presentation': { patch: [{ op: 'replace', path: '/presentation', value: '$payload' }] } },
38
+ view: {
39
+ $jslt: '0.1', modes: { assistant: { unmatched: 'error' } }, rules: [
40
+ { match: '$', body: ['section', { class: 'tangle-assistant', 'data-assistant-instance': id }, { $apply: ['$.ui.assistant', 'assistant'] }] },
41
+ ...createAssistantRules(`${id}-models`),
42
+ ]
43
+ },
44
+ }, {
45
+ node, document: node?.ownerDocument, schedule: options.schedule, onError: options.onError,
46
+ eventFields: formEventFields(), effects: controller.effects,
47
+ afterRender: () => { if (node?.querySelectorAll)
48
+ md.hydrate(node); },
49
+ viewModel: (state) => ({
50
+ ui: {
51
+ assistant: {
52
+ ...assistantView(state.ai, renderMarkdown), ...state.presentation,
53
+ capabilities: state.presentation.capabilities.map((text) => ({ text }))
54
+ }
55
+ }
56
+ }),
57
+ });
58
+ // A Markdown fragment belongs to this component, not to its host's hash router.
59
+ function click(event) {
60
+ const anchor = event.target.closest?.('a[href^="#"]');
61
+ const href = anchor?.getAttribute('href');
62
+ if (!href || href.startsWith('#/'))
63
+ return;
64
+ event.preventDefault();
65
+ let fragment;
66
+ try {
67
+ fragment = decodeURIComponent(href.slice(1));
68
+ }
69
+ catch {
70
+ return;
71
+ }
72
+ for (const target of node?.querySelectorAll('[id]') ?? []) {
73
+ if (target.id === fragment) {
74
+ target.scrollIntoView?.({ block: 'nearest' });
75
+ break;
76
+ }
77
+ }
66
78
  }
67
- }
68
- node?.addEventListener('click', click);
69
- if (options.open) {
70
- controller.effects['ai-ensure-settings']({}, app.dispatch);
71
- controller.effects['ai-ledger-read']({}, app.dispatch);
72
- }
73
- return {
74
- controller,
75
- read: () => structuredClone(app.getState().ai),
76
- dispatch(action, payload = null, event = null) { if (!disposed) app.dispatch(action, payload, event); },
77
- subscribe(listener) { return app.subscribe(state => listener(structuredClone(state.ai))); },
78
- /** Presentation updates retain the transcript, request and input node. */
79
- update(/** @type {AssistantPresentation} */ next) {
80
- if (!disposed) app.dispatch('assistant/presentation', { ...app.getState().presentation, ...presentation(next) });
81
- },
82
- dispose() {
83
- if (disposed) return closing;
84
- disposed = true; node?.removeEventListener('click', click);
85
- closing = controller.dispose(); app.destroy(); return closing;
86
- },
87
- };
79
+ node?.addEventListener('click', click);
80
+ if (options.open) {
81
+ controller.effects['ai-ensure-settings']({}, app.dispatch);
82
+ controller.effects['ai-ledger-read']({}, app.dispatch);
83
+ }
84
+ return {
85
+ controller,
86
+ read: () => structuredClone(app.getState().ai),
87
+ dispatch(action, payload = null, event = null) { if (!disposed)
88
+ app.dispatch(action, payload, event); },
89
+ subscribe(listener) { return app.subscribe((state) => listener(structuredClone(state.ai))); },
90
+ /** Presentation updates retain the transcript, request and input node. */
91
+ update(next) {
92
+ if (!disposed)
93
+ app.dispatch('assistant/presentation', { ...app.getState().presentation, ...presentation(next) });
94
+ },
95
+ dispose() {
96
+ if (disposed)
97
+ return closing;
98
+ disposed = true;
99
+ node?.removeEventListener('click', click);
100
+ closing = controller.dispose();
101
+ app.destroy();
102
+ return closing;
103
+ },
104
+ };
88
105
  }
89
- /** @param {AssistantOptions} [options] */
106
+ /** @param [options] */
90
107
  export function createAssistantWidget(options = {}) {
91
- return { mount: (host, props) => mountAssistant(host, { ...options, ...props }),
92
- update: (handle, props) => handle.update(props), unmount: handle => handle.dispose() };
108
+ return {
109
+ mount: (host, props) => mountAssistant(host, { ...options, ...props }),
110
+ update: (handle, props) => handle.update(props), unmount: (handle) => handle.dispose()
111
+ };
93
112
  }
@@ -4,13 +4,8 @@
4
4
  * injected `aiFetch` keeps the whole thing testable against a scripted
5
5
  * transport, and the injected `ledger` keeps it testable without a
6
6
  * store.
7
- * @param {{ toolbox: any, getApp: () => any,
8
- * aiFetch?: typeof fetch,
9
- * aiStorage: { read: () => any, write: (data: any) => any },
10
- * aiChat: { read: () => any, write: (data: any) => any },
11
- * ledger: any, system?: string, headers?: Record<string, string>, onDispose?: () => any }} deps
12
7
  */
13
- export function createAssistantController(deps: {
8
+ export declare function createAssistantController(deps: {
14
9
  toolbox: any;
15
10
  getApp: () => any;
16
11
  aiFetch?: typeof fetch;