@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/CHANGELOG.md +42 -0
- package/package.json +8 -8
- package/src/actions.d.ts +36 -12
- package/src/actions.js +141 -142
- package/src/component.d.ts +8 -8
- package/src/component.js +95 -76
- package/src/controller.d.ts +1 -6
- package/src/controller.js +282 -286
- package/src/index.d.ts +4 -4
- package/src/index.js +4 -4
- package/src/settings.d.ts +2 -2
- package/src/settings.js +8 -7
- package/src/state.d.ts +16 -16
- package/src/state.js +34 -35
- package/src/viewmodel.d.ts +3 -3
- package/src/viewmodel.js +71 -73
- package/src/views.d.ts +21 -21
- package/src/views.js +194 -196
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
|
|
10
|
-
import { createAssistantRules } from
|
|
11
|
-
import { assistantView } from
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
18
|
+
/** @param value */
|
|
29
19
|
function presentation(value) {
|
|
30
|
-
|
|
20
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => Object.hasOwn(DEFAULT_PRESENTATION, key)));
|
|
31
21
|
}
|
|
32
|
-
/** @param
|
|
22
|
+
/** @param node @param [options] */
|
|
33
23
|
export function mountAssistant(node, options = {}) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
106
|
+
/** @param [options] */
|
|
90
107
|
export function createAssistantWidget(options = {}) {
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
return {
|
|
109
|
+
mount: (host, props) => mountAssistant(host, { ...options, ...props }),
|
|
110
|
+
update: (handle, props) => handle.update(props), unmount: (handle) => handle.dispose()
|
|
111
|
+
};
|
|
93
112
|
}
|
package/src/controller.d.ts
CHANGED
|
@@ -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;
|