@dxos/plugin-automation 0.7.5-main.9d2a38b → 0.7.5-main.e94eead
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/dist/lib/browser/AutomationPanel-ITYXSN5Z.mjs +132 -0
- package/dist/lib/browser/AutomationPanel-ITYXSN5Z.mjs.map +7 -0
- package/dist/lib/browser/{chunk-HKX3D3ZP.mjs → chunk-OFDNNRLE.mjs} +4 -7
- package/dist/lib/browser/{chunk-HKX3D3ZP.mjs.map → chunk-OFDNNRLE.mjs.map} +3 -3
- package/dist/lib/browser/chunk-U7QLNY2S.mjs +8 -0
- package/dist/lib/browser/chunk-U7QLNY2S.mjs.map +7 -0
- package/dist/lib/browser/chunk-WY2C7JY4.mjs +119 -0
- package/dist/lib/browser/chunk-WY2C7JY4.mjs.map +7 -0
- package/dist/lib/browser/complementary-panel-4CPOJL4Y.mjs +56 -0
- package/dist/lib/browser/complementary-panel-4CPOJL4Y.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +21 -313
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{react-surface-LL72F3F4.mjs → react-surface-SAPMN4PF.mjs} +4 -11
- package/dist/lib/browser/react-surface-SAPMN4PF.mjs.map +7 -0
- package/dist/lib/node/{AutomationPanel-PPODB5XA.cjs → AutomationPanel-X33HHDMQ.cjs} +19 -117
- package/dist/lib/node/AutomationPanel-X33HHDMQ.cjs.map +7 -0
- package/dist/lib/node/chunk-7Q5SNGCL.cjs +148 -0
- package/dist/lib/node/chunk-7Q5SNGCL.cjs.map +7 -0
- package/dist/lib/node/{chunk-25AQIF3L.cjs → chunk-CB5OB6JH.cjs} +5 -8
- package/dist/lib/node/chunk-CB5OB6JH.cjs.map +7 -0
- package/dist/lib/node/{chunk-5VF5JKUN.cjs → chunk-ORMEYEBE.cjs} +7 -10
- package/dist/lib/node/{chunk-5VF5JKUN.cjs.map → chunk-ORMEYEBE.cjs.map} +3 -3
- package/dist/lib/node/complementary-panel-ZYJJ42ZU.cjs +72 -0
- package/dist/lib/node/complementary-panel-ZYJJ42ZU.cjs.map +7 -0
- package/dist/lib/node/index.cjs +26 -318
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/{react-surface-DVAU4MGD.cjs → react-surface-2WRVAPGR.cjs} +9 -15
- package/dist/lib/node/react-surface-2WRVAPGR.cjs.map +7 -0
- package/dist/lib/node-esm/AutomationPanel-HY3GB4BT.mjs +133 -0
- package/dist/lib/node-esm/AutomationPanel-HY3GB4BT.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-X3LPRWIL.mjs → chunk-6MUUH67V.mjs} +4 -7
- package/dist/lib/node-esm/{chunk-X3LPRWIL.mjs.map → chunk-6MUUH67V.mjs.map} +3 -3
- package/dist/lib/node-esm/{chunk-XW7TEQA3.mjs → chunk-R3P2WPBQ.mjs} +2 -4
- package/dist/lib/node-esm/chunk-R3P2WPBQ.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-UDD2VA6G.mjs +120 -0
- package/dist/lib/node-esm/chunk-UDD2VA6G.mjs.map +7 -0
- package/dist/lib/node-esm/complementary-panel-S42RIIAY.mjs +57 -0
- package/dist/lib/node-esm/complementary-panel-S42RIIAY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +21 -313
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{react-surface-4BED2PZ4.mjs → react-surface-QWLPOYXO.mjs} +4 -11
- package/dist/lib/node-esm/react-surface-QWLPOYXO.mjs.map +7 -0
- package/dist/types/src/AutomationPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/complementary-panel.d.ts +4 -0
- package/dist/types/src/capabilities/complementary-panel.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +2 -178
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/components/AutomationPanel/AutomationPanel.d.ts +1 -2
- package/dist/types/src/components/AutomationPanel/AutomationPanel.d.ts.map +1 -1
- package/dist/types/src/components/AutomationPanel/AutomationPanel.stories.d.ts.map +1 -1
- package/dist/types/src/components/TriggerEditor/TriggerEditor.d.ts +1 -2
- package/dist/types/src/components/TriggerEditor/TriggerEditor.d.ts.map +1 -1
- package/dist/types/src/components/TriggerEditor/TriggerEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +0 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/{testing.d.ts → test-functions.d.ts} +1 -1
- package/dist/types/src/testing/test-functions.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +2 -20
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +23 -57
- package/src/AutomationPlugin.tsx +10 -42
- package/src/capabilities/complementary-panel.ts +56 -0
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/react-surface.tsx +1 -6
- package/src/components/AutomationPanel/AutomationPanel.stories.tsx +1 -2
- package/src/components/AutomationPanel/AutomationPanel.tsx +56 -49
- package/src/components/TriggerEditor/TriggerEditor.stories.tsx +1 -2
- package/src/components/TriggerEditor/TriggerEditor.tsx +25 -18
- package/src/components/index.ts +2 -1
- package/src/index.ts +2 -3
- package/src/meta.ts +3 -3
- package/src/testing/index.ts +1 -1
- package/src/testing/{testing.ts → test-functions.ts} +2 -2
- package/src/translations.ts +2 -23
- package/dist/lib/browser/AssistantPanel-N276BTPV.mjs +0 -339
- package/dist/lib/browser/AssistantPanel-N276BTPV.mjs.map +0 -7
- package/dist/lib/browser/AutomationPanel-IHZ4JKVS.mjs +0 -232
- package/dist/lib/browser/AutomationPanel-IHZ4JKVS.mjs.map +0 -7
- package/dist/lib/browser/app-graph-builder-IJTTULDP.mjs +0 -131
- package/dist/lib/browser/app-graph-builder-IJTTULDP.mjs.map +0 -7
- package/dist/lib/browser/chunk-4AIMDHKY.mjs +0 -10
- package/dist/lib/browser/chunk-4AIMDHKY.mjs.map +0 -7
- package/dist/lib/browser/chunk-DL6LB2NI.mjs +0 -43
- package/dist/lib/browser/chunk-DL6LB2NI.mjs.map +0 -7
- package/dist/lib/browser/react-surface-LL72F3F4.mjs.map +0 -7
- package/dist/lib/browser/types/index.mjs +0 -13
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node/AssistantPanel-Z4GVHUF3.cjs +0 -359
- package/dist/lib/node/AssistantPanel-Z4GVHUF3.cjs.map +0 -7
- package/dist/lib/node/AutomationPanel-PPODB5XA.cjs.map +0 -7
- package/dist/lib/node/app-graph-builder-MF5M4QRS.cjs +0 -147
- package/dist/lib/node/app-graph-builder-MF5M4QRS.cjs.map +0 -7
- package/dist/lib/node/chunk-25AQIF3L.cjs.map +0 -7
- package/dist/lib/node/chunk-JNDMZQH7.cjs +0 -68
- package/dist/lib/node/chunk-JNDMZQH7.cjs.map +0 -7
- package/dist/lib/node/react-surface-DVAU4MGD.cjs.map +0 -7
- package/dist/lib/node/types/index.cjs +0 -35
- package/dist/lib/node/types/index.cjs.map +0 -7
- package/dist/lib/node-esm/AssistantPanel-DDCQHBJX.mjs +0 -340
- package/dist/lib/node-esm/AssistantPanel-DDCQHBJX.mjs.map +0 -7
- package/dist/lib/node-esm/AutomationPanel-R3D6CRF5.mjs +0 -233
- package/dist/lib/node-esm/AutomationPanel-R3D6CRF5.mjs.map +0 -7
- package/dist/lib/node-esm/app-graph-builder-5N7OK23B.mjs +0 -132
- package/dist/lib/node-esm/app-graph-builder-5N7OK23B.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-GIAYUM5I.mjs +0 -45
- package/dist/lib/node-esm/chunk-GIAYUM5I.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XW7TEQA3.mjs.map +0 -7
- package/dist/lib/node-esm/react-surface-4BED2PZ4.mjs.map +0 -7
- package/dist/lib/node-esm/types/index.mjs +0 -14
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -180
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
- package/dist/types/src/components/AssistantPanel/AssistantPanel.d.ts +0 -8
- package/dist/types/src/components/AssistantPanel/AssistantPanel.d.ts.map +0 -1
- package/dist/types/src/components/AssistantPanel/index.d.ts +0 -3
- package/dist/types/src/components/AssistantPanel/index.d.ts.map +0 -1
- package/dist/types/src/components/AssistantPanel/system-instructions.d.ts +0 -6
- package/dist/types/src/components/AssistantPanel/system-instructions.d.ts.map +0 -1
- package/dist/types/src/components/PromptEditor/PromptEditor.d.ts +0 -10
- package/dist/types/src/components/PromptEditor/PromptEditor.d.ts.map +0 -1
- package/dist/types/src/components/PromptEditor/PromptEditor.stories.d.ts +0 -6
- package/dist/types/src/components/PromptEditor/PromptEditor.stories.d.ts.map +0 -1
- package/dist/types/src/components/PromptEditor/index.d.ts +0 -2
- package/dist/types/src/components/PromptEditor/index.d.ts.map +0 -1
- package/dist/types/src/components/PromptEditor/prompt-extension.d.ts +0 -4
- package/dist/types/src/components/PromptEditor/prompt-extension.d.ts.map +0 -1
- package/dist/types/src/components/PromptEditor/types.d.ts +0 -18
- package/dist/types/src/components/PromptEditor/types.d.ts.map +0 -1
- package/dist/types/src/hooks/email.d.ts +0 -4
- package/dist/types/src/hooks/email.d.ts.map +0 -1
- package/dist/types/src/hooks/index.d.ts +0 -2
- package/dist/types/src/hooks/index.d.ts.map +0 -1
- package/dist/types/src/hooks/invocation-handler.d.ts +0 -5
- package/dist/types/src/hooks/invocation-handler.d.ts.map +0 -1
- package/dist/types/src/hooks/useLocalTriggerManager.d.ts +0 -3
- package/dist/types/src/hooks/useLocalTriggerManager.d.ts.map +0 -1
- package/dist/types/src/presets.d.ts +0 -9
- package/dist/types/src/presets.d.ts.map +0 -1
- package/dist/types/src/testing/testing.d.ts.map +0 -1
- package/dist/types/src/types/index.d.ts +0 -2
- package/dist/types/src/types/index.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -63
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/src/capabilities/app-graph-builder.ts +0 -127
- package/src/components/AssistantPanel/AssistantPanel.tsx +0 -230
- package/src/components/AssistantPanel/index.ts +0 -7
- package/src/components/AssistantPanel/system-instructions.ts +0 -166
- package/src/components/PromptEditor/PromptEditor.stories.tsx +0 -64
- package/src/components/PromptEditor/PromptEditor.tsx +0 -222
- package/src/components/PromptEditor/index.ts +0 -5
- package/src/components/PromptEditor/prompt-extension.ts +0 -43
- package/src/components/PromptEditor/types.tsx +0 -28
- package/src/hooks/email.ts +0 -49
- package/src/hooks/index.ts +0 -5
- package/src/hooks/invocation-handler.ts +0 -109
- package/src/hooks/useLocalTriggerManager.ts +0 -82
- package/src/presets.ts +0 -248
- package/src/types/index.ts +0 -5
- package/src/types/schema.ts +0 -38
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
/* eslint-disable no-console */
|
|
6
|
-
|
|
7
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
import { type AIServiceClient, AIServiceClientImpl, ObjectId, type Message } from '@dxos/assistant';
|
|
10
|
-
import type { ReactiveEchoObject } from '@dxos/echo-db';
|
|
11
|
-
import { SpaceId } from '@dxos/keys';
|
|
12
|
-
import { useClient, useConfig } from '@dxos/react-client';
|
|
13
|
-
import { ContextMenu, type ThemedClassName } from '@dxos/react-ui';
|
|
14
|
-
import { Icon, Input, Toolbar, useTranslation } from '@dxos/react-ui';
|
|
15
|
-
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
16
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
-
|
|
18
|
-
import { createSystemInstructions } from './system-instructions';
|
|
19
|
-
import { AUTOMATION_PLUGIN } from '../../meta';
|
|
20
|
-
|
|
21
|
-
const PROPERTIES_ASSISTANT_KEY = 'dxos.assistant.beta.properties';
|
|
22
|
-
|
|
23
|
-
export type AssistantPanelProps = ThemedClassName<{
|
|
24
|
-
subject?: ReactiveEchoObject<any>;
|
|
25
|
-
}>;
|
|
26
|
-
|
|
27
|
-
export const AssistantPanel = ({ subject, classNames }: AssistantPanelProps) => {
|
|
28
|
-
const { t } = useTranslation(AUTOMATION_PLUGIN);
|
|
29
|
-
const config = useConfig();
|
|
30
|
-
const client = useClient();
|
|
31
|
-
const aiClient = useRef<AIServiceClient>();
|
|
32
|
-
const [contextSpaceId, setContextSpaceId] = useState<SpaceId | undefined>();
|
|
33
|
-
const [threadId, setThreadId] = useState<ObjectId | undefined>();
|
|
34
|
-
const [history, setHistory] = useState<Message[]>([]);
|
|
35
|
-
const [input, setInput] = useState('');
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!aiClient.current) {
|
|
39
|
-
const endpoint = config.values.runtime?.services?.ai?.server;
|
|
40
|
-
if (!endpoint) {
|
|
41
|
-
throw new Error('AI service endpoint is not configured');
|
|
42
|
-
}
|
|
43
|
-
aiClient.current = new AIServiceClientImpl({
|
|
44
|
-
endpoint,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
queueMicrotask(async () => {
|
|
49
|
-
const properties = client.spaces.default.properties;
|
|
50
|
-
|
|
51
|
-
properties[PROPERTIES_ASSISTANT_KEY] ??= {};
|
|
52
|
-
properties[PROPERTIES_ASSISTANT_KEY].contextSpaceId ??= SpaceId.random();
|
|
53
|
-
properties[PROPERTIES_ASSISTANT_KEY].threadId ??= ObjectId.random();
|
|
54
|
-
|
|
55
|
-
const contextSpaceId = properties[PROPERTIES_ASSISTANT_KEY].contextSpaceId;
|
|
56
|
-
const threadId = properties[PROPERTIES_ASSISTANT_KEY].threadId;
|
|
57
|
-
|
|
58
|
-
setContextSpaceId(contextSpaceId);
|
|
59
|
-
setThreadId(threadId);
|
|
60
|
-
|
|
61
|
-
const messages = await aiClient.current!.getMessagesInThread(contextSpaceId, threadId);
|
|
62
|
-
setHistory(messages);
|
|
63
|
-
});
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
const handleRequest = async (input: string) => {
|
|
67
|
-
if (input === '') {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setInput('');
|
|
72
|
-
|
|
73
|
-
// TODO(dmaretskyi): Can we call `create(Message, { ... })` here?
|
|
74
|
-
const userMessage: Message = {
|
|
75
|
-
id: ObjectId.random(),
|
|
76
|
-
spaceId: contextSpaceId!,
|
|
77
|
-
threadId: threadId!,
|
|
78
|
-
role: 'user',
|
|
79
|
-
content: [{ type: 'text', text: input }],
|
|
80
|
-
};
|
|
81
|
-
await aiClient.current!.insertMessages([userMessage]);
|
|
82
|
-
setHistory([...history, userMessage]);
|
|
83
|
-
|
|
84
|
-
const generationStream = await aiClient.current!.generate({
|
|
85
|
-
model: '@anthropic/claude-3-5-sonnet-20241022',
|
|
86
|
-
spaceId: contextSpaceId!,
|
|
87
|
-
threadId: threadId!,
|
|
88
|
-
tools: [],
|
|
89
|
-
systemPrompt: await getSystemPrompt(),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const historyBefore = [...history, userMessage];
|
|
93
|
-
for await (const _event of generationStream) {
|
|
94
|
-
setHistory([...historyBefore, ...generationStream.accumulatedMessages]);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await aiClient.current!.insertMessages(await generationStream.complete());
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const getSystemPrompt = async () => {
|
|
101
|
-
return createSystemInstructions({ subject });
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const clearThread = async () => {
|
|
105
|
-
const properties = client.spaces.default.properties;
|
|
106
|
-
|
|
107
|
-
properties[PROPERTIES_ASSISTANT_KEY] ??= {};
|
|
108
|
-
// properties[PROPERTIES_ASSISTANT_KEY].contextSpaceId ??= SpaceId.random();
|
|
109
|
-
properties[PROPERTIES_ASSISTANT_KEY].threadId = ObjectId.random();
|
|
110
|
-
|
|
111
|
-
// const contextSpaceId = properties[PROPERTIES_ASSISTANT_KEY].contextSpaceId;
|
|
112
|
-
const threadId = properties[PROPERTIES_ASSISTANT_KEY].threadId;
|
|
113
|
-
|
|
114
|
-
// setContextSpaceId(contextSpaceId);
|
|
115
|
-
setThreadId(threadId);
|
|
116
|
-
|
|
117
|
-
const messages = await aiClient.current!.getMessagesInThread(contextSpaceId!, threadId);
|
|
118
|
-
setHistory(messages);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// TODO(burdon): Factor out with script plugin.
|
|
122
|
-
return (
|
|
123
|
-
<div className={mx('flex flex-col h-full overflow-hidden', classNames)}>
|
|
124
|
-
{history.length > 0 && (
|
|
125
|
-
<div className='flex flex-col gap-6 h-full p-2 overflow-x-hidden overflow-y-auto'>
|
|
126
|
-
{history.map((message) => (
|
|
127
|
-
<MessageItem key={message.id} message={message} />
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
|
|
132
|
-
<Toolbar.Root classNames='p-1'>
|
|
133
|
-
<Input.Root>
|
|
134
|
-
<Input.TextInput
|
|
135
|
-
autoFocus
|
|
136
|
-
placeholder={t('ask me anything')}
|
|
137
|
-
value={input}
|
|
138
|
-
onChange={(ev) => setInput(ev.target.value)}
|
|
139
|
-
onKeyDown={(ev) => ev.key === 'Enter' && handleRequest(input)}
|
|
140
|
-
/>
|
|
141
|
-
</Input.Root>
|
|
142
|
-
<ContextMenu.Root>
|
|
143
|
-
<ContextMenu.Trigger asChild>
|
|
144
|
-
<Toolbar.Button onClick={() => handleRequest(input)}>
|
|
145
|
-
<Icon icon='ph--play--regular' size={4} />
|
|
146
|
-
</Toolbar.Button>
|
|
147
|
-
</ContextMenu.Trigger>
|
|
148
|
-
<ContextMenu.Portal>
|
|
149
|
-
<ContextMenu.Content>
|
|
150
|
-
<ContextMenu.Viewport>
|
|
151
|
-
<ContextMenu.Item onClick={clearThread}>Clear thread</ContextMenu.Item>
|
|
152
|
-
<ContextMenu.Item onClick={async () => console.log(await getSystemPrompt())}>
|
|
153
|
-
Print instructions to console
|
|
154
|
-
</ContextMenu.Item>
|
|
155
|
-
</ContextMenu.Viewport>
|
|
156
|
-
</ContextMenu.Content>
|
|
157
|
-
</ContextMenu.Portal>
|
|
158
|
-
</ContextMenu.Root>
|
|
159
|
-
|
|
160
|
-
{/* <Toolbar.Button onClick={() => (state ? handleStop() : handleClear())}>
|
|
161
|
-
<Icon icon={state ? 'ph--stop--regular' : 'ph--trash--regular'} size={4} />
|
|
162
|
-
</Toolbar.Button> */}
|
|
163
|
-
</Toolbar.Root>
|
|
164
|
-
</div>
|
|
165
|
-
);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const MessageItem = ({ classNames, message }: ThemedClassName<{ message: Message }>) => {
|
|
169
|
-
const { id: _, role, content } = message;
|
|
170
|
-
const styleContainer = 'flex flex-col overflow-x-hidden overflow-y-auto rounded-md gap-2 divide-y divide-separator';
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div className={mx('flex', role === 'user' ? 'ml-[1rem] justify-end' : 'mr-[1rem]', classNames)}>
|
|
174
|
-
{content.map((content, i) => {
|
|
175
|
-
switch (content.type) {
|
|
176
|
-
case 'text': {
|
|
177
|
-
const { cot, message } = parseMessage(content.text);
|
|
178
|
-
return (
|
|
179
|
-
<div
|
|
180
|
-
key={i}
|
|
181
|
-
role='none'
|
|
182
|
-
className={mx(
|
|
183
|
-
styleContainer,
|
|
184
|
-
role === 'user' ? 'bg-primary-400 dark:bg-primary-600' : 'bg-hoverSurface',
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
{cot && <div className='p-2 whitespace-pre-wrap text-xs text-subdued'>{cot}</div>}
|
|
188
|
-
<div className='p-2 whitespace-pre-wrap'>{message}</div>
|
|
189
|
-
</div>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
case 'tool_use': {
|
|
194
|
-
return (
|
|
195
|
-
<div key={i} className={mx(styleContainer, 'text-xs')}>
|
|
196
|
-
<div>
|
|
197
|
-
<span className='p-2 text-primary'>Tool use</span>: {content.name} {content.id}
|
|
198
|
-
</div>
|
|
199
|
-
<SyntaxHighlighter language='json'>{content.inputJson}</SyntaxHighlighter>
|
|
200
|
-
</div>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
case 'tool_result': {
|
|
205
|
-
return (
|
|
206
|
-
<div key={i} className={mx(styleContainer, 'text-xs', content.isError && 'text-error')}>
|
|
207
|
-
<div>
|
|
208
|
-
<span className='p-2 text-primary'>Tool result</span>: {content.toolUseId}
|
|
209
|
-
</div>
|
|
210
|
-
<SyntaxHighlighter language='json'>{content.content}</SyntaxHighlighter>
|
|
211
|
-
</div>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return null;
|
|
217
|
-
})}
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// TODO(burdon): Move to server-side parsing.
|
|
223
|
-
const parseMessage = (text: string): { cot?: string; message: string } => {
|
|
224
|
-
const regex = /<cot>([\s\S]*?)<\/cot>\s*([\s\S]*)/;
|
|
225
|
-
const match = text.match(regex);
|
|
226
|
-
return {
|
|
227
|
-
cot: match?.[1].trim(),
|
|
228
|
-
message: match?.[2] ?? text ?? '\u00D8',
|
|
229
|
-
};
|
|
230
|
-
};
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { asyncTimeout } from '@dxos/async';
|
|
6
|
-
import type { ReactiveEchoObject } from '@dxos/echo-db';
|
|
7
|
-
import { getTypename } from '@dxos/echo-schema';
|
|
8
|
-
import { log } from '@dxos/log';
|
|
9
|
-
import { Filter, getSpace, ResultFormat } from '@dxos/react-client/echo';
|
|
10
|
-
|
|
11
|
-
// TODO(burdon): Move into assistant-protocol.
|
|
12
|
-
export type ThreadContext = {
|
|
13
|
-
subject?: ReactiveEchoObject<any>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const createSystemInstructions = async (context: ThreadContext): Promise<string> => {
|
|
17
|
-
let instructions = `
|
|
18
|
-
<instructions>
|
|
19
|
-
Before replying always think step-by-step on how to proceed.
|
|
20
|
-
Print your thoughts inside <cot> tags.
|
|
21
|
-
|
|
22
|
-
<example>
|
|
23
|
-
<cot>To answer the question I need to ...</cot>
|
|
24
|
-
</example>
|
|
25
|
-
</instructions>
|
|
26
|
-
|
|
27
|
-
<current_time>${new Date().toLocaleString()}</current_time>
|
|
28
|
-
`;
|
|
29
|
-
|
|
30
|
-
if (context.subject) {
|
|
31
|
-
instructions += `
|
|
32
|
-
<user_attention>
|
|
33
|
-
The user is currently interacting with an object in Composer application:
|
|
34
|
-
|
|
35
|
-
${await formatContextObject(context.subject)}
|
|
36
|
-
</user_attention>
|
|
37
|
-
`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return looseFormatXml(instructions);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const formatContextObject = async (object: ReactiveEchoObject<any>): Promise<string> => {
|
|
44
|
-
let data;
|
|
45
|
-
try {
|
|
46
|
-
data = await asyncTimeout(preprocessContextObject(object), CONTEXT_OBJECT_QUERY_TIMEOUT);
|
|
47
|
-
} catch (err: any) {
|
|
48
|
-
log.error('Failed to preprocess context object:', { err });
|
|
49
|
-
data = object;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (typeof data === 'string') {
|
|
53
|
-
return data;
|
|
54
|
-
} else {
|
|
55
|
-
return `
|
|
56
|
-
<object>
|
|
57
|
-
<type>${getTypename(object)}</type>
|
|
58
|
-
<id>${object.id}</id>
|
|
59
|
-
${formatObjectAsXMLTags(data)}
|
|
60
|
-
</object>
|
|
61
|
-
`;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const preprocessContextObject = async (object: ReactiveEchoObject<any>): Promise<Record<string, any> | string> => {
|
|
66
|
-
const space = getSpace(object);
|
|
67
|
-
if (!space) {
|
|
68
|
-
return { ...object };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// TODO(dmaretskyi): Serialize based on schema annotations.
|
|
72
|
-
switch (getTypename(object)) {
|
|
73
|
-
// TODO(dmaretskyi): Reference types somehow without plugin-automation depending on other plugins.
|
|
74
|
-
case 'dxos.org/type/Document': {
|
|
75
|
-
const data = space.db
|
|
76
|
-
.query({ id: object.id }, { format: ResultFormat.Plain, include: { content: true } })
|
|
77
|
-
.first() ?? { content: { content: '' } };
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
...data,
|
|
81
|
-
threads: undefined,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
case 'dxos.org/type/Table': {
|
|
86
|
-
// TODO(dmaretskyi): Load references.
|
|
87
|
-
const schema = object.view ? space?.db.schemaRegistry.getSchema(object.view.query.type) : undefined;
|
|
88
|
-
const { objects: rows } =
|
|
89
|
-
(schema &&
|
|
90
|
-
(await space.db
|
|
91
|
-
.query(Filter.schema(schema), { format: ResultFormat.Plain, limit: TABLE_ROWS_LIMIT })
|
|
92
|
-
.run())) ??
|
|
93
|
-
{};
|
|
94
|
-
|
|
95
|
-
// TODO(dmaretskyi): Format table schema.
|
|
96
|
-
return `
|
|
97
|
-
<object>
|
|
98
|
-
<id>${object.id}</id>
|
|
99
|
-
<type>${getTypename(object)}</type>
|
|
100
|
-
${formatObjectAsXMLTags(object)}
|
|
101
|
-
|
|
102
|
-
<rows>
|
|
103
|
-
<!-- Limited to first ${TABLE_ROWS_LIMIT} rows. -->
|
|
104
|
-
${rows
|
|
105
|
-
?.map(
|
|
106
|
-
(row: any) => `<row>
|
|
107
|
-
${formatObjectAsXMLTags(row)}
|
|
108
|
-
</row>`,
|
|
109
|
-
)
|
|
110
|
-
.join('\n')}
|
|
111
|
-
</rows>
|
|
112
|
-
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
default:
|
|
117
|
-
return { ...object };
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const formatObjectAsXMLTags = (object: any, depth = 1): string => {
|
|
122
|
-
return Object.entries(object)
|
|
123
|
-
.filter(([key, value]) => ['string', 'number', 'boolean', 'object'].includes(typeof value))
|
|
124
|
-
.map(([key, value]) => {
|
|
125
|
-
if (typeof value === 'object' && value !== null) {
|
|
126
|
-
if (depth === 0) {
|
|
127
|
-
return '';
|
|
128
|
-
} else {
|
|
129
|
-
return `<${key}>
|
|
130
|
-
${formatObjectAsXMLTags(value, depth - 1)}
|
|
131
|
-
</${key}>`;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return `<${key}>${value}</${key}>`;
|
|
136
|
-
})
|
|
137
|
-
.join('\n');
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const CONTEXT_OBJECT_QUERY_TIMEOUT = 5_000;
|
|
141
|
-
|
|
142
|
-
const TABLE_ROWS_LIMIT = 10;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Formats XML indentation for instructions so they are easier to read during debugging.
|
|
146
|
-
*/
|
|
147
|
-
const looseFormatXml = (xml: string): string => {
|
|
148
|
-
let currentIndent = 0;
|
|
149
|
-
|
|
150
|
-
return xml
|
|
151
|
-
.split('\n')
|
|
152
|
-
.map((line) => {
|
|
153
|
-
if (line.match(RE_CLOSE_TAG_LINE)) {
|
|
154
|
-
currentIndent--;
|
|
155
|
-
}
|
|
156
|
-
const indent = currentIndent;
|
|
157
|
-
if (line.match(RE_OPEN_TAG_LINE)) {
|
|
158
|
-
currentIndent++;
|
|
159
|
-
}
|
|
160
|
-
return ' '.repeat(indent * 2) + line.trimStart();
|
|
161
|
-
})
|
|
162
|
-
.join('\n');
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const RE_OPEN_TAG_LINE = /^[ ]*<[a-zA-Z0-9\-_]+>[ ]*$/;
|
|
166
|
-
const RE_CLOSE_TAG_LINE = /^[ ]*<\/[a-zA-Z0-9\-_]+>[ ]*$/;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
|
-
import { type Meta } from '@storybook/react';
|
|
8
|
-
import React, { useState } from 'react';
|
|
9
|
-
|
|
10
|
-
import { create, makeRef } from '@dxos/live-object';
|
|
11
|
-
import { useClient } from '@dxos/react-client';
|
|
12
|
-
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
-
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
14
|
-
|
|
15
|
-
import { PromptEditor } from './PromptEditor';
|
|
16
|
-
import translations from '../../translations';
|
|
17
|
-
import { ChainPromptType, ChainType } from '../../types';
|
|
18
|
-
|
|
19
|
-
const template = [
|
|
20
|
-
'# Comment',
|
|
21
|
-
'',
|
|
22
|
-
'You are a machine that is an expert chess player.',
|
|
23
|
-
'The move history of the current game is: {history}',
|
|
24
|
-
'If asked to suggest a move explain why it is a good move.',
|
|
25
|
-
'',
|
|
26
|
-
'---',
|
|
27
|
-
'',
|
|
28
|
-
'{input}',
|
|
29
|
-
].join('\n');
|
|
30
|
-
|
|
31
|
-
const DefaultStory = () => {
|
|
32
|
-
const client = useClient();
|
|
33
|
-
const [chain] = useState(() => {
|
|
34
|
-
const space = client.spaces.default;
|
|
35
|
-
return space.db.add(
|
|
36
|
-
create(ChainType, {
|
|
37
|
-
prompts: [makeRef(create(ChainPromptType, { command: 'test', template, inputs: [] }))],
|
|
38
|
-
}),
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div role='none' className='flex w-[350px] border border-separator overflow-hidden'>
|
|
44
|
-
<PromptEditor prompt={chain.prompts![0]!.target!} />
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const Default = {};
|
|
50
|
-
|
|
51
|
-
const meta: Meta = {
|
|
52
|
-
title: 'plugins/plugin-automation/PromptTemplate',
|
|
53
|
-
render: DefaultStory,
|
|
54
|
-
decorators: [
|
|
55
|
-
withClientProvider({ createIdentity: true, createSpace: true, types: [ChainType, ChainPromptType] }),
|
|
56
|
-
withLayout({ fullscreen: true, classNames: 'flex justify-center m-2' }),
|
|
57
|
-
withTheme,
|
|
58
|
-
],
|
|
59
|
-
parameters: {
|
|
60
|
-
translations,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export default meta;
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Schema as S } from '@effect/schema';
|
|
6
|
-
import React, { useEffect } from 'react';
|
|
7
|
-
|
|
8
|
-
import { createDocAccessor } from '@dxos/react-client/echo';
|
|
9
|
-
import { Input, Select, useThemeContext, useTranslation } from '@dxos/react-ui';
|
|
10
|
-
import {
|
|
11
|
-
createBasicExtensions,
|
|
12
|
-
createDataExtensions,
|
|
13
|
-
createThemeExtensions,
|
|
14
|
-
useTextEditor,
|
|
15
|
-
} from '@dxos/react-ui-editor';
|
|
16
|
-
import { attentionSurface, groupBorder, mx } from '@dxos/react-ui-theme';
|
|
17
|
-
import { nonNullable } from '@dxos/util';
|
|
18
|
-
|
|
19
|
-
import { nameRegex, promptExtension } from './prompt-extension';
|
|
20
|
-
import { AUTOMATION_PLUGIN } from '../../meta';
|
|
21
|
-
import { type ChainInput, ChainInputType, type ChainPromptType } from '../../types';
|
|
22
|
-
|
|
23
|
-
const inputTypes = [
|
|
24
|
-
{
|
|
25
|
-
value: ChainInputType.VALUE,
|
|
26
|
-
label: 'Value',
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
value: ChainInputType.PASS_THROUGH,
|
|
30
|
-
label: 'Pass through',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
value: ChainInputType.RETRIEVER,
|
|
34
|
-
label: 'Retriever',
|
|
35
|
-
},
|
|
36
|
-
// {
|
|
37
|
-
// value: ChainInputType.FUNCTION,
|
|
38
|
-
// label: 'Function',
|
|
39
|
-
// },
|
|
40
|
-
// {
|
|
41
|
-
// value: ChainInputType.QUERY,
|
|
42
|
-
// label: 'Query',
|
|
43
|
-
// },
|
|
44
|
-
{
|
|
45
|
-
value: ChainInputType.RESOLVER,
|
|
46
|
-
label: 'Resolver',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
value: ChainInputType.CONTEXT,
|
|
50
|
-
label: 'Context',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
value: ChainInputType.SCHEMA,
|
|
54
|
-
label: 'Schema',
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
const getInputType = (type: string) => inputTypes.find(({ value }) => String(value) === type)?.value;
|
|
59
|
-
|
|
60
|
-
const usePromptInputs = (prompt: ChainPromptType) => {
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
const text = prompt.template ?? '';
|
|
63
|
-
if (!prompt.inputs) {
|
|
64
|
-
prompt.inputs = []; // TODO(burdon): Required?
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const regex = new RegExp(nameRegex, 'g');
|
|
68
|
-
const variables = new Set<string>([...text.matchAll(regex)].map((m) => m[1]));
|
|
69
|
-
|
|
70
|
-
// Create map of unclaimed inputs.
|
|
71
|
-
const unclaimed = new Map<string, ChainInput>(
|
|
72
|
-
prompt.inputs?.filter(nonNullable).map((input) => [input.name, input]),
|
|
73
|
-
);
|
|
74
|
-
const missing: string[] = [];
|
|
75
|
-
Array.from(variables.values()).forEach((name) => {
|
|
76
|
-
if (unclaimed.has(name)) {
|
|
77
|
-
unclaimed.delete(name);
|
|
78
|
-
} else {
|
|
79
|
-
missing.push(name);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Match or create new inputs.
|
|
84
|
-
const values = unclaimed.values();
|
|
85
|
-
missing.forEach((name) => {
|
|
86
|
-
const next = values.next().value;
|
|
87
|
-
if (next) {
|
|
88
|
-
next.name = name;
|
|
89
|
-
} else {
|
|
90
|
-
prompt.inputs?.push({ name });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Remove unclaimed (deleted) inputs.
|
|
95
|
-
// TODO(burdon): If user types incorrect name value, it will be deleted. Garbage collect?
|
|
96
|
-
for (const input of values) {
|
|
97
|
-
prompt.inputs.splice(prompt.inputs.indexOf(input), 1);
|
|
98
|
-
}
|
|
99
|
-
}, [prompt.template]);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export type PromptEditorProps = {
|
|
103
|
-
prompt: ChainPromptType;
|
|
104
|
-
commandEditable?: boolean;
|
|
105
|
-
schema?: S.Schema<any, any, any>;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export const PromptEditor = ({ prompt, commandEditable = true }: PromptEditorProps) => {
|
|
109
|
-
const { t } = useTranslation(AUTOMATION_PLUGIN);
|
|
110
|
-
const { themeMode } = useThemeContext();
|
|
111
|
-
|
|
112
|
-
const { parentRef } = useTextEditor(
|
|
113
|
-
() => ({
|
|
114
|
-
initialValue: prompt.template,
|
|
115
|
-
extensions: [
|
|
116
|
-
createDataExtensions({
|
|
117
|
-
id: prompt.id,
|
|
118
|
-
text: prompt.template !== undefined ? createDocAccessor(prompt, ['template']) : undefined,
|
|
119
|
-
}),
|
|
120
|
-
createBasicExtensions({
|
|
121
|
-
bracketMatching: false,
|
|
122
|
-
lineWrapping: true,
|
|
123
|
-
placeholder: t('template placeholder'),
|
|
124
|
-
}),
|
|
125
|
-
createThemeExtensions({
|
|
126
|
-
themeMode,
|
|
127
|
-
slots: {
|
|
128
|
-
content: { className: '!p-3' },
|
|
129
|
-
},
|
|
130
|
-
}),
|
|
131
|
-
promptExtension,
|
|
132
|
-
],
|
|
133
|
-
}),
|
|
134
|
-
[themeMode, prompt],
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
usePromptInputs(prompt);
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<div className={mx('flex flex-col w-full overflow-hidden gap-4', groupBorder)}>
|
|
141
|
-
{commandEditable && (
|
|
142
|
-
<div className='flex items-center pl-4'>
|
|
143
|
-
<span className='text-neutral-500'>/</span>
|
|
144
|
-
<Input.Root>
|
|
145
|
-
<Input.TextInput
|
|
146
|
-
placeholder={t('command placeholder')}
|
|
147
|
-
classNames={mx('is-full bg-transparent m-2')}
|
|
148
|
-
value={prompt.command ?? ''}
|
|
149
|
-
onChange={(event) => {
|
|
150
|
-
prompt.command = event.target.value.replace(/\w/g, '');
|
|
151
|
-
}}
|
|
152
|
-
/>
|
|
153
|
-
</Input.Root>
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
156
|
-
|
|
157
|
-
<div ref={parentRef} className={mx(attentionSurface, 'rounded', 'min-h-[120px]')} />
|
|
158
|
-
|
|
159
|
-
{(prompt.inputs?.length ?? 0) > 0 && (
|
|
160
|
-
<div className='flex flex-col'>
|
|
161
|
-
{/* TODO(zan): Improve layout with grid */}
|
|
162
|
-
<table className='w-full table-fixed border-collapse my-2'>
|
|
163
|
-
<tbody>
|
|
164
|
-
{prompt.inputs?.filter(nonNullable).map((input) => (
|
|
165
|
-
<tr key={input.name}>
|
|
166
|
-
<td className='w-[160px] p-1 font-mono text-sm whitespace-nowrap truncate'>
|
|
167
|
-
<code className='px-2'>{input.name}</code>
|
|
168
|
-
</td>
|
|
169
|
-
<td className='w-[120px] p-1'>
|
|
170
|
-
<Input.Root>
|
|
171
|
-
<Select.Root
|
|
172
|
-
value={String(input.type)}
|
|
173
|
-
onValueChange={(type) => {
|
|
174
|
-
input.type = getInputType(type) ?? ChainInputType.VALUE;
|
|
175
|
-
}}
|
|
176
|
-
>
|
|
177
|
-
<Select.TriggerButton placeholder='Type' classNames='is-full' />
|
|
178
|
-
<Select.Portal>
|
|
179
|
-
<Select.Content>
|
|
180
|
-
<Select.Viewport>
|
|
181
|
-
{inputTypes.map(({ value, label }) => (
|
|
182
|
-
<Select.Option key={value} value={String(value)}>
|
|
183
|
-
{label}
|
|
184
|
-
</Select.Option>
|
|
185
|
-
))}
|
|
186
|
-
</Select.Viewport>
|
|
187
|
-
</Select.Content>
|
|
188
|
-
</Select.Portal>
|
|
189
|
-
</Select.Root>
|
|
190
|
-
</Input.Root>
|
|
191
|
-
</td>
|
|
192
|
-
<td className='p-1 pr-2'>
|
|
193
|
-
{input.type !== undefined &&
|
|
194
|
-
[
|
|
195
|
-
ChainInputType.VALUE,
|
|
196
|
-
ChainInputType.CONTEXT,
|
|
197
|
-
ChainInputType.RESOLVER,
|
|
198
|
-
ChainInputType.SCHEMA,
|
|
199
|
-
].includes(input.type) && (
|
|
200
|
-
<div>
|
|
201
|
-
<Input.Root>
|
|
202
|
-
<Input.TextInput
|
|
203
|
-
placeholder={t('command placeholder')}
|
|
204
|
-
classNames={mx('is-full bg-transparent')}
|
|
205
|
-
value={input.value ?? ''}
|
|
206
|
-
onChange={(event) => {
|
|
207
|
-
input.value = event.target.value;
|
|
208
|
-
}}
|
|
209
|
-
/>
|
|
210
|
-
</Input.Root>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
</td>
|
|
214
|
-
</tr>
|
|
215
|
-
))}
|
|
216
|
-
</tbody>
|
|
217
|
-
</table>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
);
|
|
222
|
-
};
|