@humaan/payload-plugin-ai-assistant 0.0.0-stage → 0.1.0
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/README.md +277 -2
- package/dist/admin/AiAssistantControl.d.ts +11 -0
- package/dist/admin/AiAssistantControl.js +31 -0
- package/dist/admin/AiAssistantControl.js.map +1 -0
- package/dist/admin/AiAssistantPanel.d.ts +10 -0
- package/dist/admin/AiAssistantPanel.js +208 -0
- package/dist/admin/AiAssistantPanel.js.map +1 -0
- package/dist/admin/AiAssistantPanel.module.scss +689 -0
- package/dist/admin/AiSourceSwitch.d.ts +13 -0
- package/dist/admin/AiSourceSwitch.js +14 -0
- package/dist/admin/AiSourceSwitch.js.map +1 -0
- package/dist/admin/BudgetField.d.ts +8 -0
- package/dist/admin/BudgetField.js +145 -0
- package/dist/admin/BudgetField.js.map +1 -0
- package/dist/admin/BudgetField.module.scss +47 -0
- package/dist/admin/Conversation.d.ts +12 -0
- package/dist/admin/Conversation.js +469 -0
- package/dist/admin/Conversation.js.map +1 -0
- package/dist/admin/Markdown.d.ts +9 -0
- package/dist/admin/Markdown.js +63 -0
- package/dist/admin/Markdown.js.map +1 -0
- package/dist/admin/PageContextBridge.d.ts +5 -0
- package/dist/admin/PageContextBridge.js +72 -0
- package/dist/admin/PageContextBridge.js.map +1 -0
- package/dist/admin/Pill.d.ts +11 -0
- package/dist/admin/Pill.js +27 -0
- package/dist/admin/Pill.js.map +1 -0
- package/dist/admin/PlanCard.d.ts +14 -0
- package/dist/admin/PlanCard.js +465 -0
- package/dist/admin/PlanCard.js.map +1 -0
- package/dist/admin/SettingsAlerts.d.ts +7 -0
- package/dist/admin/SettingsAlerts.js +37 -0
- package/dist/admin/SettingsAlerts.js.map +1 -0
- package/dist/admin/UsageSummary.d.ts +11 -0
- package/dist/admin/UsageSummary.js +129 -0
- package/dist/admin/UsageSummary.js.map +1 -0
- package/dist/admin/attachmentUpload.d.ts +25 -0
- package/dist/admin/attachmentUpload.js +76 -0
- package/dist/admin/attachmentUpload.js.map +1 -0
- package/dist/admin/openDocument.d.ts +13 -0
- package/dist/admin/openDocument.js +34 -0
- package/dist/admin/openDocument.js.map +1 -0
- package/dist/admin/percent.d.ts +10 -0
- package/dist/admin/percent.js +21 -0
- package/dist/admin/percent.js.map +1 -0
- package/dist/admin/threads.d.ts +21 -0
- package/dist/admin/threads.js +92 -0
- package/dist/admin/threads.js.map +1 -0
- package/dist/admin/toolSteps.d.ts +12 -0
- package/dist/admin/toolSteps.js +57 -0
- package/dist/admin/toolSteps.js.map +1 -0
- package/dist/admin/transport.d.ts +24 -0
- package/dist/admin/transport.js +75 -0
- package/dist/admin/transport.js.map +1 -0
- package/dist/admin/useAttachments.d.ts +32 -0
- package/dist/admin/useAttachments.js +91 -0
- package/dist/admin/useAttachments.js.map +1 -0
- package/dist/admin/useEntityLabel.d.ts +3 -0
- package/dist/admin/useEntityLabel.js +23 -0
- package/dist/admin/useEntityLabel.js.map +1 -0
- package/dist/attachments/collection.d.ts +15 -0
- package/dist/attachments/collection.js +90 -0
- package/dist/attachments/collection.js.map +1 -0
- package/dist/attachments/conversation.d.ts +25 -0
- package/dist/attachments/conversation.js +105 -0
- package/dist/attachments/conversation.js.map +1 -0
- package/dist/attachments/rules.d.ts +9 -0
- package/dist/attachments/rules.js +17 -0
- package/dist/attachments/rules.js.map +1 -0
- package/dist/attachments/store.d.ts +27 -0
- package/dist/attachments/store.js +187 -0
- package/dist/attachments/store.js.map +1 -0
- package/dist/billing.d.ts +60 -0
- package/dist/billing.js +116 -0
- package/dist/billing.js.map +1 -0
- package/dist/chat/history.d.ts +37 -0
- package/dist/chat/history.js +165 -0
- package/dist/chat/history.js.map +1 -0
- package/dist/chat/messages.d.ts +5 -0
- package/dist/chat/messages.js +3 -0
- package/dist/chat/messages.js.map +1 -0
- package/dist/chat/threads.d.ts +33 -0
- package/dist/chat/threads.js +77 -0
- package/dist/chat/threads.js.map +1 -0
- package/dist/chat/tools.d.ts +76 -0
- package/dist/chat/tools.js +596 -0
- package/dist/chat/tools.js.map +1 -0
- package/dist/chat/usage.d.ts +66 -0
- package/dist/chat/usage.js +219 -0
- package/dist/chat/usage.js.map +1 -0
- package/dist/collectionAccess.d.ts +6 -0
- package/dist/collectionAccess.js +32 -0
- package/dist/collectionAccess.js.map +1 -0
- package/dist/constants.d.ts +64 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/endpoints/access.d.ts +4 -0
- package/dist/endpoints/access.js +28 -0
- package/dist/endpoints/access.js.map +1 -0
- package/dist/endpoints/allowance.d.ts +22 -0
- package/dist/endpoints/allowance.js +37 -0
- package/dist/endpoints/allowance.js.map +1 -0
- package/dist/endpoints/applyEndpoint.d.ts +12 -0
- package/dist/endpoints/applyEndpoint.js +170 -0
- package/dist/endpoints/applyEndpoint.js.map +1 -0
- package/dist/endpoints/budget.d.ts +30 -0
- package/dist/endpoints/budget.js +153 -0
- package/dist/endpoints/budget.js.map +1 -0
- package/dist/endpoints/chatEndpoint.d.ts +12 -0
- package/dist/endpoints/chatEndpoint.js +295 -0
- package/dist/endpoints/chatEndpoint.js.map +1 -0
- package/dist/endpoints/form.d.ts +23 -0
- package/dist/endpoints/form.js +95 -0
- package/dist/endpoints/form.js.map +1 -0
- package/dist/endpoints/usageEndpoint.d.ts +13 -0
- package/dist/endpoints/usageEndpoint.js +70 -0
- package/dist/endpoints/usageEndpoint.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lexical/editorConfig.d.ts +4 -0
- package/dist/lexical/editorConfig.js +19 -0
- package/dist/lexical/editorConfig.js.map +1 -0
- package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
- package/dist/lexical/lexicalToMarkdown.js +10 -0
- package/dist/lexical/lexicalToMarkdown.js.map +1 -0
- package/dist/lexical/markdownToLexical.d.ts +7 -0
- package/dist/lexical/markdownToLexical.js +10 -0
- package/dist/lexical/markdownToLexical.js.map +1 -0
- package/dist/plan/allowed.d.ts +7 -0
- package/dist/plan/allowed.js +19 -0
- package/dist/plan/allowed.js.map +1 -0
- package/dist/plan/applyPlan.d.ts +16 -0
- package/dist/plan/applyPlan.js +353 -0
- package/dist/plan/applyPlan.js.map +1 -0
- package/dist/plan/catalogue.d.ts +37 -0
- package/dist/plan/catalogue.js +157 -0
- package/dist/plan/catalogue.js.map +1 -0
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +87 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/parsePlan.d.ts +27 -0
- package/dist/plan/parsePlan.js +322 -0
- package/dist/plan/parsePlan.js.map +1 -0
- package/dist/plan/prompt.d.ts +1 -0
- package/dist/plan/prompt.js +45 -0
- package/dist/plan/prompt.js.map +1 -0
- package/dist/plan/stored.d.ts +53 -0
- package/dist/plan/stored.js +3 -0
- package/dist/plan/stored.js.map +1 -0
- package/dist/plan/tokens.d.ts +8 -0
- package/dist/plan/tokens.js +79 -0
- package/dist/plan/tokens.js.map +1 -0
- package/dist/plan/view.d.ts +4 -0
- package/dist/plan/view.js +43 -0
- package/dist/plan/view.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +464 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/outline.d.ts +7 -0
- package/dist/read/outline.js +70 -0
- package/dist/read/outline.js.map +1 -0
- package/dist/read/readTarget.d.ts +4 -0
- package/dist/read/readTarget.js +13 -0
- package/dist/read/readTarget.js.map +1 -0
- package/dist/read/readable.d.ts +8 -0
- package/dist/read/readable.js +131 -0
- package/dist/read/readable.js.map +1 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +14 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema/fields.d.ts +113 -0
- package/dist/schema/fields.js +1036 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/listingFields.d.ts +9 -0
- package/dist/schema/listingFields.js +95 -0
- package/dist/schema/listingFields.js.map +1 -0
- package/dist/schema/slugify.d.ts +2 -0
- package/dist/schema/slugify.js +5 -0
- package/dist/schema/slugify.js.map +1 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +54 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +198 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/uploads/routeAsset.d.ts +27 -0
- package/dist/uploads/routeAsset.js +127 -0
- package/dist/uploads/routeAsset.js.map +1 -0
- package/package.json +122 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AssistantUIMessage } from '../chat/messages.js';
|
|
2
|
+
import type { PlanTargetKind } from '../types.js';
|
|
3
|
+
export type EntityLabel = (slug: string, kind?: PlanTargetKind) => string;
|
|
4
|
+
type Part = AssistantUIMessage['parts'][number];
|
|
5
|
+
export interface ToolStep {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
state: 'done' | 'failed' | 'running';
|
|
9
|
+
}
|
|
10
|
+
/** A line in the assistant's working steps, for every tool call except a successful plan, which shows as a card. */
|
|
11
|
+
export declare function toolStep(part: Part, entityLabel: EntityLabel): ToolStep | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getToolName, isToolUIPart } from 'ai';
|
|
2
|
+
/** A line in the assistant's working steps, for every tool call except a successful plan, which shows as a card. */ export function toolStep(part, entityLabel) {
|
|
3
|
+
if (!isToolUIPart(part)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const name = getToolName(part);
|
|
7
|
+
const input = part.input ?? {};
|
|
8
|
+
const state = part.state === 'output-error' ? 'failed' : part.state === 'output-available' ? 'done' : 'running';
|
|
9
|
+
if (name === 'submitPlan') {
|
|
10
|
+
if (part.state === 'output-available' && isRecord(part.output) && part.output.ok === true) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const label = part.state === 'output-available' ? 'Corrected the plan' : 'Preparing the plan';
|
|
14
|
+
return {
|
|
15
|
+
id: part.toolCallId,
|
|
16
|
+
label,
|
|
17
|
+
state: part.state === 'output-available' ? 'done' : state
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const output = part.state === 'output-available' && isRecord(part.output) ? part.output : {};
|
|
21
|
+
return {
|
|
22
|
+
id: part.toolCallId,
|
|
23
|
+
label: labelFor(name, input, output, entityLabel),
|
|
24
|
+
state
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function labelFor(name, input, output, entityLabel) {
|
|
28
|
+
const slug = text(input.slug) || text(input.collection);
|
|
29
|
+
const kind = input.kind === 'global' ? 'global' : 'collection';
|
|
30
|
+
const entity = slug ? entityLabel(slug, kind) : '';
|
|
31
|
+
switch(name){
|
|
32
|
+
case 'describeTarget':
|
|
33
|
+
return `Read the ${entity} fields`;
|
|
34
|
+
case 'listTargets':
|
|
35
|
+
return 'Checked which collections can change';
|
|
36
|
+
case 'readDocument':
|
|
37
|
+
{
|
|
38
|
+
const title = text(output.label);
|
|
39
|
+
return kind === 'global' || !title ? `Read ${entity}` : `Read ${entity} “${title.slice(0, 60)}”`;
|
|
40
|
+
}
|
|
41
|
+
case 'searchDocuments':
|
|
42
|
+
{
|
|
43
|
+
const query = text(input.query).slice(0, 60);
|
|
44
|
+
return query ? `Searched ${entity} for “${query}”` : `Looked through ${entity}`;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function text(value) {
|
|
51
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
52
|
+
}
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=toolSteps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/toolSteps.ts"],"sourcesContent":["import { getToolName, isToolUIPart } from 'ai'\n\nimport type { AssistantUIMessage } from '../chat/messages.js'\nimport type { PlanTargetKind } from '../types.js'\n\nexport type EntityLabel = (slug: string, kind?: PlanTargetKind) => string\n\ntype Part = AssistantUIMessage['parts'][number]\n\nexport interface ToolStep {\n id: string\n label: string\n state: 'done' | 'failed' | 'running'\n}\n\n/** A line in the assistant's working steps, for every tool call except a successful plan, which shows as a card. */\nexport function toolStep(part: Part, entityLabel: EntityLabel): ToolStep | undefined {\n if (!isToolUIPart(part)) {\n return undefined\n }\n const name = getToolName(part)\n const input = (part.input ?? {}) as Record<string, unknown>\n const state =\n part.state === 'output-error'\n ? 'failed'\n : part.state === 'output-available'\n ? 'done'\n : 'running'\n\n if (name === 'submitPlan') {\n if (part.state === 'output-available' && isRecord(part.output) && part.output.ok === true) {\n return undefined\n }\n const label = part.state === 'output-available' ? 'Corrected the plan' : 'Preparing the plan'\n return { id: part.toolCallId, label, state: part.state === 'output-available' ? 'done' : state }\n }\n const output = part.state === 'output-available' && isRecord(part.output) ? part.output : {}\n return { id: part.toolCallId, label: labelFor(name, input, output, entityLabel), state }\n}\n\nfunction labelFor(\n name: string,\n input: Record<string, unknown>,\n output: Record<string, unknown>,\n entityLabel: EntityLabel,\n): string {\n const slug = text(input.slug) || text(input.collection)\n const kind = input.kind === 'global' ? 'global' : 'collection'\n const entity = slug ? entityLabel(slug, kind) : ''\n switch (name) {\n case 'describeTarget':\n return `Read the ${entity} fields`\n case 'listTargets':\n return 'Checked which collections can change'\n case 'readDocument': {\n const title = text(output.label)\n return kind === 'global' || !title\n ? `Read ${entity}`\n : `Read ${entity} “${title.slice(0, 60)}”`\n }\n case 'searchDocuments': {\n const query = text(input.query).slice(0, 60)\n return query ? `Searched ${entity} for “${query}”` : `Looked through ${entity}`\n }\n default:\n return name\n }\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["getToolName","isToolUIPart","toolStep","part","entityLabel","undefined","name","input","state","isRecord","output","ok","label","id","toolCallId","labelFor","slug","text","collection","kind","entity","title","slice","query","value","trim","Boolean","Array","isArray"],"mappings":"AAAA,SAASA,WAAW,EAAEC,YAAY,QAAQ,KAAI;AAe9C,kHAAkH,GAClH,OAAO,SAASC,SAASC,IAAU,EAAEC,WAAwB;IAC3D,IAAI,CAACH,aAAaE,OAAO;QACvB,OAAOE;IACT;IACA,MAAMC,OAAON,YAAYG;IACzB,MAAMI,QAASJ,KAAKI,KAAK,IAAI,CAAC;IAC9B,MAAMC,QACJL,KAAKK,KAAK,KAAK,iBACX,WACAL,KAAKK,KAAK,KAAK,qBACb,SACA;IAER,IAAIF,SAAS,cAAc;QACzB,IAAIH,KAAKK,KAAK,KAAK,sBAAsBC,SAASN,KAAKO,MAAM,KAAKP,KAAKO,MAAM,CAACC,EAAE,KAAK,MAAM;YACzF,OAAON;QACT;QACA,MAAMO,QAAQT,KAAKK,KAAK,KAAK,qBAAqB,uBAAuB;QACzE,OAAO;YAAEK,IAAIV,KAAKW,UAAU;YAAEF;YAAOJ,OAAOL,KAAKK,KAAK,KAAK,qBAAqB,SAASA;QAAM;IACjG;IACA,MAAME,SAASP,KAAKK,KAAK,KAAK,sBAAsBC,SAASN,KAAKO,MAAM,IAAIP,KAAKO,MAAM,GAAG,CAAC;IAC3F,OAAO;QAAEG,IAAIV,KAAKW,UAAU;QAAEF,OAAOG,SAAST,MAAMC,OAAOG,QAAQN;QAAcI;IAAM;AACzF;AAEA,SAASO,SACPT,IAAY,EACZC,KAA8B,EAC9BG,MAA+B,EAC/BN,WAAwB;IAExB,MAAMY,OAAOC,KAAKV,MAAMS,IAAI,KAAKC,KAAKV,MAAMW,UAAU;IACtD,MAAMC,OAAOZ,MAAMY,IAAI,KAAK,WAAW,WAAW;IAClD,MAAMC,SAASJ,OAAOZ,YAAYY,MAAMG,QAAQ;IAChD,OAAQb;QACN,KAAK;YACH,OAAO,CAAC,SAAS,EAAEc,OAAO,OAAO,CAAC;QACpC,KAAK;YACH,OAAO;QACT,KAAK;YAAgB;gBACnB,MAAMC,QAAQJ,KAAKP,OAAOE,KAAK;gBAC/B,OAAOO,SAAS,YAAY,CAACE,QACzB,CAAC,KAAK,EAAED,QAAQ,GAChB,CAAC,KAAK,EAAEA,OAAO,EAAE,EAAEC,MAAMC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;YAC9C;QACA,KAAK;YAAmB;gBACtB,MAAMC,QAAQN,KAAKV,MAAMgB,KAAK,EAAED,KAAK,CAAC,GAAG;gBACzC,OAAOC,QAAQ,CAAC,SAAS,EAAEH,OAAO,MAAM,EAAEG,MAAM,CAAC,CAAC,GAAG,CAAC,eAAe,EAAEH,QAAQ;YACjF;QACA;YACE,OAAOd;IACX;AACF;AAEA,SAASW,KAAKO,KAAc;IAC1B,OAAO,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AACpD;AAEA,SAAShB,SAASe,KAAc;IAC9B,OAAOE,QAAQF,UAAU,OAAOA,UAAU,YAAY,CAACG,MAAMC,OAAO,CAACJ;AACvE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UIMessageChunk } from 'ai';
|
|
2
|
+
import { DefaultChatTransport } from 'ai';
|
|
3
|
+
import type { AssistantUIMessage } from '../chat/messages.js';
|
|
4
|
+
import type { OpenDocument } from './openDocument.js';
|
|
5
|
+
interface AssistantTransportOptions {
|
|
6
|
+
api: string;
|
|
7
|
+
openDocument: () => OpenDocument | undefined;
|
|
8
|
+
}
|
|
9
|
+
type SendOptions = Parameters<DefaultChatTransport<AssistantUIMessage>['sendMessages']>[0];
|
|
10
|
+
/**
|
|
11
|
+
* Sends the new message as form data, with the open document and the ids of the conversation's attachments.
|
|
12
|
+
* The files themselves are already in storage, so requests stay small.
|
|
13
|
+
* The server keeps the history, so earlier messages are not sent again.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AssistantTransport extends DefaultChatTransport<AssistantUIMessage> {
|
|
16
|
+
/** Ids of the conversation's stored attachments. */
|
|
17
|
+
private attachmentIds;
|
|
18
|
+
private readonly openDocument;
|
|
19
|
+
constructor({ api, openDocument }: AssistantTransportOptions);
|
|
20
|
+
reconnectToStream(): Promise<null>;
|
|
21
|
+
sendMessages({ chatId, messages, abortSignal, }: SendOptions): Promise<ReadableStream<UIMessageChunk>>;
|
|
22
|
+
setAttachmentIds(ids: readonly string[]): void;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DefaultChatTransport } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* Sends the new message as form data, with the open document and the ids of the conversation's attachments.
|
|
4
|
+
* The files themselves are already in storage, so requests stay small.
|
|
5
|
+
* The server keeps the history, so earlier messages are not sent again.
|
|
6
|
+
*/ export class AssistantTransport extends DefaultChatTransport {
|
|
7
|
+
/** Ids of the conversation's stored attachments. */ attachmentIds = [];
|
|
8
|
+
openDocument;
|
|
9
|
+
constructor({ api, openDocument }){
|
|
10
|
+
super({
|
|
11
|
+
api,
|
|
12
|
+
credentials: 'include'
|
|
13
|
+
});
|
|
14
|
+
this.openDocument = openDocument;
|
|
15
|
+
}
|
|
16
|
+
reconnectToStream() {
|
|
17
|
+
return Promise.resolve(null);
|
|
18
|
+
}
|
|
19
|
+
async sendMessages({ chatId, messages, abortSignal }) {
|
|
20
|
+
const message = messages.at(-1);
|
|
21
|
+
if (!message || message.role !== 'user') {
|
|
22
|
+
throw new Error('There is no message to send.');
|
|
23
|
+
}
|
|
24
|
+
const body = new FormData();
|
|
25
|
+
body.set('chatId', chatId);
|
|
26
|
+
body.set('message', JSON.stringify({
|
|
27
|
+
id: message.id,
|
|
28
|
+
role: message.role,
|
|
29
|
+
parts: message.parts
|
|
30
|
+
}));
|
|
31
|
+
const open = this.openDocument();
|
|
32
|
+
if (open) {
|
|
33
|
+
body.set('context', JSON.stringify(open.context));
|
|
34
|
+
const data = readFormData(open);
|
|
35
|
+
if (data) {
|
|
36
|
+
body.set('contextData', data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (this.attachmentIds.length > 0) {
|
|
40
|
+
body.set('attachmentIds', JSON.stringify(this.attachmentIds));
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(this.api, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body,
|
|
45
|
+
credentials: 'include',
|
|
46
|
+
signal: abortSignal
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(await errorMessage(response));
|
|
50
|
+
}
|
|
51
|
+
if (!response.body) {
|
|
52
|
+
throw new Error('The assistant could not be reached. Try again.');
|
|
53
|
+
}
|
|
54
|
+
return this.processResponseStream(response.body);
|
|
55
|
+
}
|
|
56
|
+
setAttachmentIds(ids) {
|
|
57
|
+
this.attachmentIds = ids;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readFormData(open) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.stringify(open.getData());
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function errorMessage(response) {
|
|
68
|
+
const payload = await response.json().catch(()=>null);
|
|
69
|
+
if (payload && typeof payload === 'object' && 'message' in payload && typeof payload.message === 'string') {
|
|
70
|
+
return payload.message;
|
|
71
|
+
}
|
|
72
|
+
return 'The assistant could not be reached. Try again.';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/transport.ts"],"sourcesContent":["import type { UIMessageChunk } from 'ai'\n\nimport { DefaultChatTransport } from 'ai'\n\nimport type { AssistantUIMessage } from '../chat/messages.js'\nimport type { OpenDocument } from './openDocument.js'\n\ninterface AssistantTransportOptions {\n api: string\n openDocument: () => OpenDocument | undefined\n}\n\ntype SendOptions = Parameters<DefaultChatTransport<AssistantUIMessage>['sendMessages']>[0]\n\n/**\n * Sends the new message as form data, with the open document and the ids of the conversation's attachments.\n * The files themselves are already in storage, so requests stay small.\n * The server keeps the history, so earlier messages are not sent again.\n */\nexport class AssistantTransport extends DefaultChatTransport<AssistantUIMessage> {\n /** Ids of the conversation's stored attachments. */\n private attachmentIds: readonly string[] = []\n private readonly openDocument: AssistantTransportOptions['openDocument']\n\n constructor({ api, openDocument }: AssistantTransportOptions) {\n super({ api, credentials: 'include' })\n this.openDocument = openDocument\n }\n\n override reconnectToStream(): Promise<null> {\n return Promise.resolve(null)\n }\n\n override async sendMessages({\n chatId,\n messages,\n abortSignal,\n }: SendOptions): Promise<ReadableStream<UIMessageChunk>> {\n const message = messages.at(-1)\n if (!message || message.role !== 'user') {\n throw new Error('There is no message to send.')\n }\n\n const body = new FormData()\n body.set('chatId', chatId)\n body.set(\n 'message',\n JSON.stringify({ id: message.id, role: message.role, parts: message.parts }),\n )\n const open = this.openDocument()\n if (open) {\n body.set('context', JSON.stringify(open.context))\n const data = readFormData(open)\n if (data) {\n body.set('contextData', data)\n }\n }\n if (this.attachmentIds.length > 0) {\n body.set('attachmentIds', JSON.stringify(this.attachmentIds))\n }\n\n const response = await fetch(this.api, {\n method: 'POST',\n body,\n credentials: 'include',\n signal: abortSignal,\n })\n if (!response.ok) {\n throw new Error(await errorMessage(response))\n }\n if (!response.body) {\n throw new Error('The assistant could not be reached. Try again.')\n }\n return this.processResponseStream(response.body)\n }\n\n setAttachmentIds(ids: readonly string[]): void {\n this.attachmentIds = ids\n }\n}\n\nfunction readFormData(open: OpenDocument): string | undefined {\n try {\n return JSON.stringify(open.getData())\n } catch {\n return undefined\n }\n}\n\nasync function errorMessage(response: Response): Promise<string> {\n const payload: unknown = await response.json().catch(() => null)\n if (\n payload &&\n typeof payload === 'object' &&\n 'message' in payload &&\n typeof payload.message === 'string'\n ) {\n return payload.message\n }\n return 'The assistant could not be reached. Try again.'\n}\n"],"names":["DefaultChatTransport","AssistantTransport","attachmentIds","openDocument","api","credentials","reconnectToStream","Promise","resolve","sendMessages","chatId","messages","abortSignal","message","at","role","Error","body","FormData","set","JSON","stringify","id","parts","open","context","data","readFormData","length","response","fetch","method","signal","ok","errorMessage","processResponseStream","setAttachmentIds","ids","getData","undefined","payload","json","catch"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,KAAI;AAYzC;;;;CAIC,GACD,OAAO,MAAMC,2BAA2BD;IACtC,kDAAkD,GAClD,AAAQE,gBAAmC,EAAE,CAAA;IAC5BC,aAAuD;IAExE,YAAY,EAAEC,GAAG,EAAED,YAAY,EAA6B,CAAE;QAC5D,KAAK,CAAC;YAAEC;YAAKC,aAAa;QAAU;QACpC,IAAI,CAACF,YAAY,GAAGA;IACtB;IAESG,oBAAmC;QAC1C,OAAOC,QAAQC,OAAO,CAAC;IACzB;IAEA,MAAeC,aAAa,EAC1BC,MAAM,EACNC,QAAQ,EACRC,WAAW,EACC,EAA2C;QACvD,MAAMC,UAAUF,SAASG,EAAE,CAAC,CAAC;QAC7B,IAAI,CAACD,WAAWA,QAAQE,IAAI,KAAK,QAAQ;YACvC,MAAM,IAAIC,MAAM;QAClB;QAEA,MAAMC,OAAO,IAAIC;QACjBD,KAAKE,GAAG,CAAC,UAAUT;QACnBO,KAAKE,GAAG,CACN,WACAC,KAAKC,SAAS,CAAC;YAAEC,IAAIT,QAAQS,EAAE;YAAEP,MAAMF,QAAQE,IAAI;YAAEQ,OAAOV,QAAQU,KAAK;QAAC;QAE5E,MAAMC,OAAO,IAAI,CAACrB,YAAY;QAC9B,IAAIqB,MAAM;YACRP,KAAKE,GAAG,CAAC,WAAWC,KAAKC,SAAS,CAACG,KAAKC,OAAO;YAC/C,MAAMC,OAAOC,aAAaH;YAC1B,IAAIE,MAAM;gBACRT,KAAKE,GAAG,CAAC,eAAeO;YAC1B;QACF;QACA,IAAI,IAAI,CAACxB,aAAa,CAAC0B,MAAM,GAAG,GAAG;YACjCX,KAAKE,GAAG,CAAC,iBAAiBC,KAAKC,SAAS,CAAC,IAAI,CAACnB,aAAa;QAC7D;QAEA,MAAM2B,WAAW,MAAMC,MAAM,IAAI,CAAC1B,GAAG,EAAE;YACrC2B,QAAQ;YACRd;YACAZ,aAAa;YACb2B,QAAQpB;QACV;QACA,IAAI,CAACiB,SAASI,EAAE,EAAE;YAChB,MAAM,IAAIjB,MAAM,MAAMkB,aAAaL;QACrC;QACA,IAAI,CAACA,SAASZ,IAAI,EAAE;YAClB,MAAM,IAAID,MAAM;QAClB;QACA,OAAO,IAAI,CAACmB,qBAAqB,CAACN,SAASZ,IAAI;IACjD;IAEAmB,iBAAiBC,GAAsB,EAAQ;QAC7C,IAAI,CAACnC,aAAa,GAAGmC;IACvB;AACF;AAEA,SAASV,aAAaH,IAAkB;IACtC,IAAI;QACF,OAAOJ,KAAKC,SAAS,CAACG,KAAKc,OAAO;IACpC,EAAE,OAAM;QACN,OAAOC;IACT;AACF;AAEA,eAAeL,aAAaL,QAAkB;IAC5C,MAAMW,UAAmB,MAAMX,SAASY,IAAI,GAAGC,KAAK,CAAC,IAAM;IAC3D,IACEF,WACA,OAAOA,YAAY,YACnB,aAAaA,WACb,OAAOA,QAAQ3B,OAAO,KAAK,UAC3B;QACA,OAAO2B,QAAQ3B,OAAO;IACxB;IACA,OAAO;AACT"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { UploadedAttachment } from './attachmentUpload.js';
|
|
2
|
+
export interface ComposerAttachment {
|
|
3
|
+
error?: string;
|
|
4
|
+
/** Set once stored. */
|
|
5
|
+
id?: string;
|
|
6
|
+
key: string;
|
|
7
|
+
name: string;
|
|
8
|
+
size: number;
|
|
9
|
+
status: 'failed' | 'ready' | 'uploading';
|
|
10
|
+
type: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Files attached in a conversation. Each one uploads as soon as it is picked, so sending a message carries only ids.
|
|
14
|
+
* Attachments already sent stay with the conversation; ones removed before sending are deleted.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useAttachments({ apiRoute, initial, }: {
|
|
17
|
+
apiRoute: string;
|
|
18
|
+
initial: readonly UploadedAttachment[];
|
|
19
|
+
}): {
|
|
20
|
+
items: ComposerAttachment[];
|
|
21
|
+
add: (files: readonly File[]) => number;
|
|
22
|
+
remove: (key: string) => void;
|
|
23
|
+
/** Every stored attachment. The server ignores ids the conversation already has, so sending them all is safe. */
|
|
24
|
+
readyIds: string[];
|
|
25
|
+
unsent: ({
|
|
26
|
+
id: string;
|
|
27
|
+
} & ComposerAttachment)[];
|
|
28
|
+
sentIds: ReadonlySet<string>;
|
|
29
|
+
uploading: boolean;
|
|
30
|
+
full: boolean;
|
|
31
|
+
markSent: (ids: readonly string[]) => void;
|
|
32
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useUploadHandlers } from '@payloadcms/ui';
|
|
3
|
+
import { generateId } from 'ai';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { attachmentProblem } from '../attachments/rules.js';
|
|
6
|
+
import { aiAssistantAttachmentSlug, maxFiles } from '../constants.js';
|
|
7
|
+
import { removeAttachment, uploadAttachment } from './attachmentUpload.js';
|
|
8
|
+
/**
|
|
9
|
+
* Files attached in a conversation. Each one uploads as soon as it is picked, so sending a message carries only ids.
|
|
10
|
+
* Attachments already sent stay with the conversation; ones removed before sending are deleted.
|
|
11
|
+
*/ export function useAttachments({ apiRoute, initial }) {
|
|
12
|
+
const { getUploadHandler } = useUploadHandlers();
|
|
13
|
+
const [items, setItems] = useState(()=>initial.map((attachment)=>({
|
|
14
|
+
key: attachment.id,
|
|
15
|
+
...attachment,
|
|
16
|
+
status: 'ready'
|
|
17
|
+
})));
|
|
18
|
+
const [sentIds, setSentIds] = useState(()=>new Set(initial.map((attachment)=>attachment.id)));
|
|
19
|
+
const update = (key, change)=>setItems((current)=>current.map((item)=>item.key === key ? {
|
|
20
|
+
...item,
|
|
21
|
+
...change
|
|
22
|
+
} : item));
|
|
23
|
+
/** Starts uploading the picked files. Returns how many were left out because the conversation is full. */ const add = (files)=>{
|
|
24
|
+
const room = Math.max(0, maxFiles - items.filter((item)=>item.status !== 'failed').length);
|
|
25
|
+
const accepted = files.slice(0, room);
|
|
26
|
+
const handler = getUploadHandler({
|
|
27
|
+
collectionSlug: aiAssistantAttachmentSlug
|
|
28
|
+
});
|
|
29
|
+
const added = accepted.map((file)=>{
|
|
30
|
+
const problem = attachmentProblem({
|
|
31
|
+
name: file.name,
|
|
32
|
+
type: file.type,
|
|
33
|
+
size: file.size
|
|
34
|
+
});
|
|
35
|
+
const item = {
|
|
36
|
+
key: generateId(),
|
|
37
|
+
name: file.name,
|
|
38
|
+
size: file.size,
|
|
39
|
+
type: file.type,
|
|
40
|
+
status: problem ? 'failed' : 'uploading',
|
|
41
|
+
...problem ? {
|
|
42
|
+
error: problem
|
|
43
|
+
} : {}
|
|
44
|
+
};
|
|
45
|
+
if (!problem) {
|
|
46
|
+
uploadAttachment({
|
|
47
|
+
apiRoute,
|
|
48
|
+
file,
|
|
49
|
+
handler
|
|
50
|
+
}).then((stored)=>update(item.key, {
|
|
51
|
+
...stored,
|
|
52
|
+
status: 'ready'
|
|
53
|
+
})).catch((error)=>update(item.key, {
|
|
54
|
+
status: 'failed',
|
|
55
|
+
error: error instanceof Error && error.message ? error.message : `${file.name} could not be uploaded.`
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
return item;
|
|
59
|
+
});
|
|
60
|
+
setItems((current)=>[
|
|
61
|
+
...current,
|
|
62
|
+
...added
|
|
63
|
+
]);
|
|
64
|
+
return files.length - accepted.length;
|
|
65
|
+
};
|
|
66
|
+
const remove = (key)=>{
|
|
67
|
+
const item = items.find((entry)=>entry.key === key);
|
|
68
|
+
setItems((current)=>current.filter((entry)=>entry.key !== key));
|
|
69
|
+
if (item?.id && !sentIds.has(item.id)) {
|
|
70
|
+
void removeAttachment(apiRoute, item.id);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const ready = items.filter((item)=>item.status === 'ready' && Boolean(item.id));
|
|
74
|
+
const unsent = ready.filter((item)=>!sentIds.has(item.id));
|
|
75
|
+
return {
|
|
76
|
+
items,
|
|
77
|
+
add,
|
|
78
|
+
remove,
|
|
79
|
+
/** Every stored attachment. The server ignores ids the conversation already has, so sending them all is safe. */ readyIds: ready.map((item)=>item.id),
|
|
80
|
+
unsent,
|
|
81
|
+
sentIds,
|
|
82
|
+
uploading: items.some((item)=>item.status === 'uploading'),
|
|
83
|
+
full: items.filter((item)=>item.status !== 'failed').length >= maxFiles,
|
|
84
|
+
markSent: (ids)=>setSentIds((current)=>new Set([
|
|
85
|
+
...current,
|
|
86
|
+
...ids
|
|
87
|
+
]))
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=useAttachments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/useAttachments.ts"],"sourcesContent":["'use client'\n\nimport type { UploadCollectionSlug } from 'payload'\n\nimport { useUploadHandlers } from '@payloadcms/ui'\nimport { generateId } from 'ai'\nimport { useState } from 'react'\n\nimport type { UploadedAttachment } from './attachmentUpload.js'\n\nimport { attachmentProblem } from '../attachments/rules.js'\nimport { aiAssistantAttachmentSlug, maxFiles } from '../constants.js'\nimport { removeAttachment, uploadAttachment } from './attachmentUpload.js'\n\nexport interface ComposerAttachment {\n error?: string\n /** Set once stored. */\n id?: string\n key: string\n name: string\n size: number\n status: 'failed' | 'ready' | 'uploading'\n type: string\n}\n\n/**\n * Files attached in a conversation. Each one uploads as soon as it is picked, so sending a message carries only ids.\n * Attachments already sent stay with the conversation; ones removed before sending are deleted.\n */\nexport function useAttachments({\n apiRoute,\n initial,\n}: {\n apiRoute: string\n initial: readonly UploadedAttachment[]\n}) {\n const { getUploadHandler } = useUploadHandlers()\n const [items, setItems] = useState<ComposerAttachment[]>(() =>\n initial.map((attachment) => ({ key: attachment.id, ...attachment, status: 'ready' })),\n )\n const [sentIds, setSentIds] = useState<ReadonlySet<string>>(\n () => new Set(initial.map((attachment) => attachment.id)),\n )\n\n const update = (key: string, change: Partial<ComposerAttachment>) =>\n setItems((current) => current.map((item) => (item.key === key ? { ...item, ...change } : item)))\n\n /** Starts uploading the picked files. Returns how many were left out because the conversation is full. */\n const add = (files: readonly File[]): number => {\n const room = Math.max(0, maxFiles - items.filter((item) => item.status !== 'failed').length)\n const accepted = files.slice(0, room)\n const handler = getUploadHandler({\n collectionSlug: aiAssistantAttachmentSlug as UploadCollectionSlug,\n })\n\n const added = accepted.map((file) => {\n const problem = attachmentProblem({ name: file.name, type: file.type, size: file.size })\n const item: ComposerAttachment = {\n key: generateId(),\n name: file.name,\n size: file.size,\n type: file.type,\n status: problem ? 'failed' : 'uploading',\n ...(problem ? { error: problem } : {}),\n }\n if (!problem) {\n uploadAttachment({ apiRoute, file, handler })\n .then((stored) => update(item.key, { ...stored, status: 'ready' }))\n .catch((error: unknown) =>\n update(item.key, {\n status: 'failed',\n error:\n error instanceof Error && error.message\n ? error.message\n : `${file.name} could not be uploaded.`,\n }),\n )\n }\n return item\n })\n setItems((current) => [...current, ...added])\n return files.length - accepted.length\n }\n\n const remove = (key: string) => {\n const item = items.find((entry) => entry.key === key)\n setItems((current) => current.filter((entry) => entry.key !== key))\n if (item?.id && !sentIds.has(item.id)) {\n void removeAttachment(apiRoute, item.id)\n }\n }\n\n const ready = items.filter(\n (item): item is { id: string } & ComposerAttachment =>\n item.status === 'ready' && Boolean(item.id),\n )\n const unsent = ready.filter((item) => !sentIds.has(item.id))\n\n return {\n items,\n add,\n remove,\n /** Every stored attachment. The server ignores ids the conversation already has, so sending them all is safe. */\n readyIds: ready.map((item) => item.id),\n unsent,\n sentIds,\n uploading: items.some((item) => item.status === 'uploading'),\n full: items.filter((item) => item.status !== 'failed').length >= maxFiles,\n markSent: (ids: readonly string[]) => setSentIds((current) => new Set([...current, ...ids])),\n }\n}\n"],"names":["useUploadHandlers","generateId","useState","attachmentProblem","aiAssistantAttachmentSlug","maxFiles","removeAttachment","uploadAttachment","useAttachments","apiRoute","initial","getUploadHandler","items","setItems","map","attachment","key","id","status","sentIds","setSentIds","Set","update","change","current","item","add","files","room","Math","max","filter","length","accepted","slice","handler","collectionSlug","added","file","problem","name","type","size","error","then","stored","catch","Error","message","remove","find","entry","has","ready","Boolean","unsent","readyIds","uploading","some","full","markSent","ids"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useConfig } from '@payloadcms/ui';
|
|
2
|
+
/** Turns a collection or global slug into the singular label editors see in the admin, such as "Page". */ export function useEntityLabel() {
|
|
3
|
+
const { config } = useConfig();
|
|
4
|
+
return (slug, kind = 'collection')=>{
|
|
5
|
+
if (kind === 'global') {
|
|
6
|
+
return labelText(config.globals.find((global)=>global.slug === slug)?.label) ?? slug;
|
|
7
|
+
}
|
|
8
|
+
const labels = config.collections.find((collection)=>collection.slug === slug)?.labels;
|
|
9
|
+
return labelText(labels?.singular) ?? slug;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function labelText(label) {
|
|
13
|
+
if (typeof label === 'string' && label) {
|
|
14
|
+
return label;
|
|
15
|
+
}
|
|
16
|
+
if (label && typeof label === 'object') {
|
|
17
|
+
const first = Object.values(label).find((value)=>typeof value === 'string' && value);
|
|
18
|
+
return typeof first === 'string' ? first : undefined;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=useEntityLabel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/admin/useEntityLabel.ts"],"sourcesContent":["import { useConfig } from '@payloadcms/ui'\n\nimport type { PlanTargetKind } from '../types.js'\n\n/** Turns a collection or global slug into the singular label editors see in the admin, such as \"Page\". */\nexport function useEntityLabel(): (slug: string, kind?: PlanTargetKind) => string {\n const { config } = useConfig()\n return (slug, kind = 'collection') => {\n if (kind === 'global') {\n return labelText(config.globals.find((global) => global.slug === slug)?.label) ?? slug\n }\n const labels = config.collections.find((collection) => collection.slug === slug)?.labels\n return labelText(labels?.singular) ?? slug\n }\n}\n\nfunction labelText(label: unknown): string | undefined {\n if (typeof label === 'string' && label) {\n return label\n }\n if (label && typeof label === 'object') {\n const first = Object.values(label).find((value) => typeof value === 'string' && value)\n return typeof first === 'string' ? first : undefined\n }\n return undefined\n}\n"],"names":["useConfig","useEntityLabel","config","slug","kind","labelText","globals","find","global","label","labels","collections","collection","singular","first","Object","values","value","undefined"],"mappings":"AAAA,SAASA,SAAS,QAAQ,iBAAgB;AAI1C,wGAAwG,GACxG,OAAO,SAASC;IACd,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,OAAO,CAACG,MAAMC,OAAO,YAAY;QAC/B,IAAIA,SAAS,UAAU;YACrB,OAAOC,UAAUH,OAAOI,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOL,IAAI,KAAKA,OAAOM,UAAUN;QACpF;QACA,MAAMO,SAASR,OAAOS,WAAW,CAACJ,IAAI,CAAC,CAACK,aAAeA,WAAWT,IAAI,KAAKA,OAAOO;QAClF,OAAOL,UAAUK,QAAQG,aAAaV;IACxC;AACF;AAEA,SAASE,UAAUI,KAAc;IAC/B,IAAI,OAAOA,UAAU,YAAYA,OAAO;QACtC,OAAOA;IACT;IACA,IAAIA,SAAS,OAAOA,UAAU,UAAU;QACtC,MAAMK,QAAQC,OAAOC,MAAM,CAACP,OAAOF,IAAI,CAAC,CAACU,QAAU,OAAOA,UAAU,YAAYA;QAChF,OAAO,OAAOH,UAAU,WAAWA,QAAQI;IAC7C;IACA,OAAOA;AACT"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CollectionConfig, CollectionSlug } from 'payload';
|
|
2
|
+
import type { AssistantSettings } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The upload collection for files attached in a conversation. Add it to the Payload config's `collections`
|
|
5
|
+
* and to your storage adapter's collections, with client uploads on, so large files go straight to storage
|
|
6
|
+
* instead of through a server request. Keep its files private.
|
|
7
|
+
*
|
|
8
|
+
* The plugin adds the owner field, access control, and file checks, so this only needs the slug and upload settings.
|
|
9
|
+
*/
|
|
10
|
+
export declare function aiAssistantAttachmentsCollection(overrides?: Partial<CollectionConfig>): CollectionConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Applied by the plugin to the project's attachments collection, after storage adapters have added theirs.
|
|
13
|
+
* Each attachment belongs to the editor who uploaded it. Only they can read or delete it, and nobody can edit it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withAttachmentRules(collection: CollectionConfig, usersSlug: CollectionSlug, fallback: AssistantSettings): CollectionConfig;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { APIError } from 'payload';
|
|
2
|
+
import { assistantAccess, ownerAccess } from '../collectionAccess.js';
|
|
3
|
+
import { aiAssistantAttachmentSlug } from '../constants.js';
|
|
4
|
+
import { userIdOf } from '../settings.js';
|
|
5
|
+
import { attachmentProblem } from './rules.js';
|
|
6
|
+
/**
|
|
7
|
+
* The upload collection for files attached in a conversation. Add it to the Payload config's `collections`
|
|
8
|
+
* and to your storage adapter's collections, with client uploads on, so large files go straight to storage
|
|
9
|
+
* instead of through a server request. Keep its files private.
|
|
10
|
+
*
|
|
11
|
+
* The plugin adds the owner field, access control, and file checks, so this only needs the slug and upload settings.
|
|
12
|
+
*/ export function aiAssistantAttachmentsCollection(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
slug: aiAssistantAttachmentSlug,
|
|
15
|
+
labels: {
|
|
16
|
+
singular: 'Humaan AI attachment',
|
|
17
|
+
plural: 'Humaan AI attachments'
|
|
18
|
+
},
|
|
19
|
+
admin: {
|
|
20
|
+
hidden: true
|
|
21
|
+
},
|
|
22
|
+
upload: true,
|
|
23
|
+
fields: [],
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Applied by the plugin to the project's attachments collection, after storage adapters have added theirs.
|
|
29
|
+
* Each attachment belongs to the editor who uploaded it. Only they can read or delete it, and nobody can edit it.
|
|
30
|
+
*/ export function withAttachmentRules(collection, usersSlug, fallback) {
|
|
31
|
+
const owned = ownerAccess(fallback);
|
|
32
|
+
return {
|
|
33
|
+
...collection,
|
|
34
|
+
admin: {
|
|
35
|
+
...collection.admin,
|
|
36
|
+
hidden: true
|
|
37
|
+
},
|
|
38
|
+
access: {
|
|
39
|
+
...collection.access,
|
|
40
|
+
create: assistantAccess(fallback),
|
|
41
|
+
read: owned,
|
|
42
|
+
update: ()=>false,
|
|
43
|
+
delete: owned
|
|
44
|
+
},
|
|
45
|
+
fields: [
|
|
46
|
+
...collection.fields,
|
|
47
|
+
{
|
|
48
|
+
name: 'owner',
|
|
49
|
+
type: 'relationship',
|
|
50
|
+
relationTo: usersSlug,
|
|
51
|
+
index: true,
|
|
52
|
+
admin: {
|
|
53
|
+
readOnly: true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
hooks: {
|
|
58
|
+
...collection.hooks,
|
|
59
|
+
beforeChange: [
|
|
60
|
+
...collection.hooks?.beforeChange ?? [],
|
|
61
|
+
checkAttachment
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sets the owner from the session and refuses programs and oversized files. With client uploads the file is
|
|
68
|
+
* already in storage by now, so the panel checks the same rules before it uploads.
|
|
69
|
+
*/ const checkAttachment = ({ data, operation, req })=>{
|
|
70
|
+
if (operation !== 'create') {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
const name = typeof data.filename === 'string' ? data.filename : '';
|
|
74
|
+
const type = typeof data.mimeType === 'string' ? data.mimeType : '';
|
|
75
|
+
const size = typeof data.filesize === 'number' ? data.filesize : 0;
|
|
76
|
+
const problem = attachmentProblem({
|
|
77
|
+
name,
|
|
78
|
+
type,
|
|
79
|
+
size
|
|
80
|
+
});
|
|
81
|
+
if (problem) {
|
|
82
|
+
throw new APIError(problem, 400, undefined, true);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...data,
|
|
86
|
+
owner: userIdOf(req.user)
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/attachments/collection.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook, CollectionConfig, CollectionSlug } from 'payload'\n\nimport { APIError } from 'payload'\n\nimport type { AssistantSettings } from '../types.js'\n\nimport { assistantAccess, ownerAccess } from '../collectionAccess.js'\nimport { aiAssistantAttachmentSlug } from '../constants.js'\nimport { userIdOf } from '../settings.js'\nimport { attachmentProblem } from './rules.js'\n\n/**\n * The upload collection for files attached in a conversation. Add it to the Payload config's `collections`\n * and to your storage adapter's collections, with client uploads on, so large files go straight to storage\n * instead of through a server request. Keep its files private.\n *\n * The plugin adds the owner field, access control, and file checks, so this only needs the slug and upload settings.\n */\nexport function aiAssistantAttachmentsCollection(\n overrides: Partial<CollectionConfig> = {},\n): CollectionConfig {\n return {\n slug: aiAssistantAttachmentSlug,\n labels: { singular: 'Humaan AI attachment', plural: 'Humaan AI attachments' },\n admin: { hidden: true },\n upload: true,\n fields: [],\n ...overrides,\n }\n}\n\n/**\n * Applied by the plugin to the project's attachments collection, after storage adapters have added theirs.\n * Each attachment belongs to the editor who uploaded it. Only they can read or delete it, and nobody can edit it.\n */\nexport function withAttachmentRules(\n collection: CollectionConfig,\n usersSlug: CollectionSlug,\n fallback: AssistantSettings,\n): CollectionConfig {\n const owned = ownerAccess(fallback)\n return {\n ...collection,\n admin: { ...collection.admin, hidden: true },\n access: {\n ...collection.access,\n create: assistantAccess(fallback),\n read: owned,\n update: () => false,\n delete: owned,\n },\n fields: [\n ...collection.fields,\n {\n name: 'owner',\n type: 'relationship',\n relationTo: usersSlug,\n index: true,\n admin: { readOnly: true },\n },\n ],\n hooks: {\n ...collection.hooks,\n beforeChange: [...(collection.hooks?.beforeChange ?? []), checkAttachment],\n },\n }\n}\n\n/**\n * Sets the owner from the session and refuses programs and oversized files. With client uploads the file is\n * already in storage by now, so the panel checks the same rules before it uploads.\n */\nconst checkAttachment: CollectionBeforeChangeHook = ({ data, operation, req }) => {\n if (operation !== 'create') {\n return data\n }\n const name = typeof data.filename === 'string' ? data.filename : ''\n const type = typeof data.mimeType === 'string' ? data.mimeType : ''\n const size = typeof data.filesize === 'number' ? data.filesize : 0\n const problem = attachmentProblem({ name, type, size })\n if (problem) {\n throw new APIError(problem, 400, undefined, true)\n }\n return { ...data, owner: userIdOf(req.user) }\n}\n"],"names":["APIError","assistantAccess","ownerAccess","aiAssistantAttachmentSlug","userIdOf","attachmentProblem","aiAssistantAttachmentsCollection","overrides","slug","labels","singular","plural","admin","hidden","upload","fields","withAttachmentRules","collection","usersSlug","fallback","owned","access","create","read","update","delete","name","type","relationTo","index","readOnly","hooks","beforeChange","checkAttachment","data","operation","req","filename","mimeType","size","filesize","problem","undefined","owner","user"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,UAAS;AAIlC,SAASC,eAAe,EAAEC,WAAW,QAAQ,yBAAwB;AACrE,SAASC,yBAAyB,QAAQ,kBAAiB;AAC3D,SAASC,QAAQ,QAAQ,iBAAgB;AACzC,SAASC,iBAAiB,QAAQ,aAAY;AAE9C;;;;;;CAMC,GACD,OAAO,SAASC,iCACdC,YAAuC,CAAC,CAAC;IAEzC,OAAO;QACLC,MAAML;QACNM,QAAQ;YAAEC,UAAU;YAAwBC,QAAQ;QAAwB;QAC5EC,OAAO;YAAEC,QAAQ;QAAK;QACtBC,QAAQ;QACRC,QAAQ,EAAE;QACV,GAAGR,SAAS;IACd;AACF;AAEA;;;CAGC,GACD,OAAO,SAASS,oBACdC,UAA4B,EAC5BC,SAAyB,EACzBC,QAA2B;IAE3B,MAAMC,QAAQlB,YAAYiB;IAC1B,OAAO;QACL,GAAGF,UAAU;QACbL,OAAO;YAAE,GAAGK,WAAWL,KAAK;YAAEC,QAAQ;QAAK;QAC3CQ,QAAQ;YACN,GAAGJ,WAAWI,MAAM;YACpBC,QAAQrB,gBAAgBkB;YACxBI,MAAMH;YACNI,QAAQ,IAAM;YACdC,QAAQL;QACV;QACAL,QAAQ;eACHE,WAAWF,MAAM;YACpB;gBACEW,MAAM;gBACNC,MAAM;gBACNC,YAAYV;gBACZW,OAAO;gBACPjB,OAAO;oBAAEkB,UAAU;gBAAK;YAC1B;SACD;QACDC,OAAO;YACL,GAAGd,WAAWc,KAAK;YACnBC,cAAc;mBAAKf,WAAWc,KAAK,EAAEC,gBAAgB,EAAE;gBAAGC;aAAgB;QAC5E;IACF;AACF;AAEA;;;CAGC,GACD,MAAMA,kBAA8C,CAAC,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;IAC3E,IAAID,cAAc,UAAU;QAC1B,OAAOD;IACT;IACA,MAAMR,OAAO,OAAOQ,KAAKG,QAAQ,KAAK,WAAWH,KAAKG,QAAQ,GAAG;IACjE,MAAMV,OAAO,OAAOO,KAAKI,QAAQ,KAAK,WAAWJ,KAAKI,QAAQ,GAAG;IACjE,MAAMC,OAAO,OAAOL,KAAKM,QAAQ,KAAK,WAAWN,KAAKM,QAAQ,GAAG;IACjE,MAAMC,UAAUpC,kBAAkB;QAAEqB;QAAMC;QAAMY;IAAK;IACrD,IAAIE,SAAS;QACX,MAAM,IAAIzC,SAASyC,SAAS,KAAKC,WAAW;IAC9C;IACA,OAAO;QAAE,GAAGR,IAAI;QAAES,OAAOvC,SAASgC,IAAIQ,IAAI;IAAE;AAC9C"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FileUIPart } from 'ai';
|
|
2
|
+
import type { PayloadRequest, SanitizedConfig } from 'payload';
|
|
3
|
+
import type { ThreadFile } from '../chat/threads.js';
|
|
4
|
+
import type { IncomingFile } from '../plan/parsePlan.js';
|
|
5
|
+
import type { AttachmentRecord } from './store.js';
|
|
6
|
+
export type AttachmentAddition = {
|
|
7
|
+
added: ThreadFile[];
|
|
8
|
+
files: ThreadFile[];
|
|
9
|
+
ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
message: string;
|
|
12
|
+
ok: false;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Adds a message's new attachments to the conversation's list. `found` is what the attachments collection returned
|
|
16
|
+
* for the requested ids, with the editor's access, so an id that is missing is someone else's or no longer exists.
|
|
17
|
+
*/
|
|
18
|
+
export declare function addAttachments(known: readonly ThreadFile[], requested: readonly string[], found: readonly AttachmentRecord[]): AttachmentAddition;
|
|
19
|
+
/**
|
|
20
|
+
* The conversation's readable attachments, with where a plan would save each one. Only upload collections the
|
|
21
|
+
* assistant may change are considered. A file none of them accepts can still be read, but not saved.
|
|
22
|
+
*/
|
|
23
|
+
export declare function conversationFiles(config: SanitizedConfig, files: readonly ThreadFile[]): IncomingFile[];
|
|
24
|
+
/** Images and PDFs, read from storage so the model can see them. A file that cannot be read is described by name only. */
|
|
25
|
+
export declare function modelFileParts(req: PayloadRequest, files: readonly IncomingFile[]): Promise<Map<number, FileUIPart>>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { maxFiles } from '../constants.js';
|
|
2
|
+
import { uploadTargets } from '../plan/catalogue.js';
|
|
3
|
+
import { chooseUploadCollection, kindFromMime } from '../uploads/routeAsset.js';
|
|
4
|
+
import { readAttachment } from './store.js';
|
|
5
|
+
/**
|
|
6
|
+
* Adds a message's new attachments to the conversation's list. `found` is what the attachments collection returned
|
|
7
|
+
* for the requested ids, with the editor's access, so an id that is missing is someone else's or no longer exists.
|
|
8
|
+
*/ export function addAttachments(known, requested, found) {
|
|
9
|
+
const knownIds = new Set(known.flatMap((file)=>file.id ? [
|
|
10
|
+
file.id
|
|
11
|
+
] : []));
|
|
12
|
+
const newIds = [
|
|
13
|
+
...new Set(requested)
|
|
14
|
+
].filter((id)=>!knownIds.has(id));
|
|
15
|
+
if (known.length + newIds.length > maxFiles) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
message: `A conversation can have up to ${maxFiles} files. Start a new conversation for more.`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const byId = new Map(found.map((record)=>[
|
|
22
|
+
record.id,
|
|
23
|
+
record
|
|
24
|
+
]));
|
|
25
|
+
const added = [];
|
|
26
|
+
for (const id of newIds){
|
|
27
|
+
const record = byId.get(id);
|
|
28
|
+
if (!record) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
message: 'An attached file could not be found. Attach it again.'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
added.push({
|
|
35
|
+
id,
|
|
36
|
+
name: record.name,
|
|
37
|
+
size: record.size,
|
|
38
|
+
type: record.type
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
files: [
|
|
44
|
+
...known,
|
|
45
|
+
...added
|
|
46
|
+
],
|
|
47
|
+
added
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The conversation's readable attachments, with where a plan would save each one. Only upload collections the
|
|
52
|
+
* assistant may change are considered. A file none of them accepts can still be read, but not saved.
|
|
53
|
+
*/ export function conversationFiles(config, files) {
|
|
54
|
+
const targets = uploadTargets(config).map((target)=>{
|
|
55
|
+
const collection = config.collections.find((item)=>item.slug === target.slug);
|
|
56
|
+
const drafts = Boolean(collection?.versions && typeof collection.versions === 'object' && collection.versions.drafts);
|
|
57
|
+
return {
|
|
58
|
+
...target,
|
|
59
|
+
drafts
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return files.flatMap((file, index)=>{
|
|
63
|
+
if (!file.id) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const choice = chooseUploadCollection(file, targets);
|
|
67
|
+
const target = 'error' in choice ? undefined : targets.find((item)=>item.slug === choice.slug);
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
index,
|
|
71
|
+
attachmentId: file.id,
|
|
72
|
+
name: file.name,
|
|
73
|
+
size: file.size,
|
|
74
|
+
type: 'error' in choice ? file.type : choice.mime,
|
|
75
|
+
collection: target?.slug ?? '',
|
|
76
|
+
kind: 'error' in choice ? kindFromMime(file.type) : choice.kind,
|
|
77
|
+
savesImmediately: !target?.drafts
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Images and PDFs, read from storage so the model can see them. A file that cannot be read is described by name only. */ export async function modelFileParts(req, files) {
|
|
83
|
+
const readable = files.filter((file)=>file.kind === 'image' || file.type === 'application/pdf');
|
|
84
|
+
const parts = new Map();
|
|
85
|
+
await Promise.all(readable.map(async (file)=>{
|
|
86
|
+
try {
|
|
87
|
+
// The type the bytes arrived as, in case storage converted the format.
|
|
88
|
+
const { data, type } = await readAttachment(req, file.attachmentId);
|
|
89
|
+
parts.set(file.index, {
|
|
90
|
+
type: 'file',
|
|
91
|
+
mediaType: type,
|
|
92
|
+
filename: file.name,
|
|
93
|
+
url: `data:${type};base64,${data.toString('base64')}`
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
req.payload.logger.warn({
|
|
97
|
+
err: error,
|
|
98
|
+
attachment: file.attachmentId
|
|
99
|
+
}, 'Humaan AI could not read an attachment');
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
return parts;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=conversation.js.map
|