iris-chatbot 5.3.0 → 5.4.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.
@@ -0,0 +1,222 @@
1
+ import type { ChatAttachment } from "./types";
2
+
3
+ export const MAX_ATTACHMENTS_PER_MESSAGE = 8;
4
+ export const MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
5
+
6
+ const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "gif"]);
7
+ const DOCUMENT_EXTENSIONS = new Set(["txt", "md", "doc", "docx"]);
8
+
9
+ const DOCUMENT_MIME_TYPES = new Set([
10
+ "text/plain",
11
+ "text/markdown",
12
+ "text/x-markdown",
13
+ "application/msword",
14
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
15
+ ]);
16
+
17
+ const EXTENSION_TO_MIME: Record<string, string> = {
18
+ png: "image/png",
19
+ jpg: "image/jpeg",
20
+ jpeg: "image/jpeg",
21
+ webp: "image/webp",
22
+ gif: "image/gif",
23
+ pdf: "application/pdf",
24
+ txt: "text/plain",
25
+ md: "text/markdown",
26
+ doc: "application/msword",
27
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
28
+ };
29
+
30
+ export const COMPOSER_FILE_ACCEPT =
31
+ ".png,.jpg,.jpeg,.webp,.gif,.pdf,.txt,.md,.doc,.docx";
32
+
33
+ type AttachmentConnection = {
34
+ kind?: unknown;
35
+ provider?: unknown;
36
+ } | null | undefined;
37
+
38
+ type AttachmentCandidate = {
39
+ name: string;
40
+ mimeType?: string | null;
41
+ sizeBytes: number;
42
+ };
43
+
44
+ function normalizeFileName(name: string): string {
45
+ const trimmed = name.trim();
46
+ if (!trimmed) {
47
+ return "attachment";
48
+ }
49
+ const basename = trimmed.split(/[\\/]/).pop()?.trim() || "attachment";
50
+ return basename || "attachment";
51
+ }
52
+
53
+ function normalizeMimeType(mimeType: string | null | undefined): string {
54
+ return typeof mimeType === "string" ? mimeType.trim().toLowerCase() : "";
55
+ }
56
+
57
+ function extensionFromName(name: string): string {
58
+ const index = name.lastIndexOf(".");
59
+ if (index < 0 || index === name.length - 1) {
60
+ return "";
61
+ }
62
+ return name.slice(index + 1).toLowerCase();
63
+ }
64
+
65
+ function inferMimeType(name: string, fallback: ChatAttachment["kind"]): string {
66
+ const ext = extensionFromName(name);
67
+ const mapped = EXTENSION_TO_MIME[ext];
68
+ if (mapped) {
69
+ return mapped;
70
+ }
71
+ if (fallback === "image") {
72
+ return "image/png";
73
+ }
74
+ if (fallback === "pdf") {
75
+ return "application/pdf";
76
+ }
77
+ return "text/plain";
78
+ }
79
+
80
+ export function getAttachmentProviderForConnection(
81
+ connection: AttachmentConnection,
82
+ ): ChatAttachment["provider"] | null {
83
+ if (!connection || typeof connection.kind !== "string") {
84
+ return null;
85
+ }
86
+ if (connection.kind === "openai_compatible") {
87
+ return "openai";
88
+ }
89
+ if (connection.kind !== "builtin" || typeof connection.provider !== "string") {
90
+ return null;
91
+ }
92
+ if (connection.provider === "openai") {
93
+ return "openai";
94
+ }
95
+ if (connection.provider === "anthropic") {
96
+ return "anthropic";
97
+ }
98
+ return null;
99
+ }
100
+
101
+ export function detectAttachmentKind(
102
+ candidate: Pick<AttachmentCandidate, "name" | "mimeType">,
103
+ ): ChatAttachment["kind"] | null {
104
+ const name = normalizeFileName(candidate.name);
105
+ const extension = extensionFromName(name);
106
+ const mimeType = normalizeMimeType(candidate.mimeType);
107
+
108
+ if (mimeType.startsWith("image/") || IMAGE_EXTENSIONS.has(extension)) {
109
+ return "image";
110
+ }
111
+ if (mimeType === "application/pdf" || extension === "pdf") {
112
+ return "pdf";
113
+ }
114
+ if (DOCUMENT_MIME_TYPES.has(mimeType) || DOCUMENT_EXTENSIONS.has(extension)) {
115
+ return "document";
116
+ }
117
+ return null;
118
+ }
119
+
120
+ export function validateAttachmentCandidate(candidate: AttachmentCandidate): {
121
+ ok: true;
122
+ kind: ChatAttachment["kind"];
123
+ name: string;
124
+ mimeType: string;
125
+ sizeBytes: number;
126
+ } | {
127
+ ok: false;
128
+ error: string;
129
+ } {
130
+ const name = normalizeFileName(candidate.name);
131
+ if (!Number.isFinite(candidate.sizeBytes) || candidate.sizeBytes <= 0) {
132
+ return {
133
+ ok: false,
134
+ error: `${name} is empty.`,
135
+ };
136
+ }
137
+ if (candidate.sizeBytes > MAX_ATTACHMENT_SIZE_BYTES) {
138
+ return {
139
+ ok: false,
140
+ error: `${name} is larger than 25 MB.`,
141
+ };
142
+ }
143
+
144
+ const kind = detectAttachmentKind(candidate);
145
+ if (!kind) {
146
+ return {
147
+ ok: false,
148
+ error: `${name} is not a supported file type.`,
149
+ };
150
+ }
151
+
152
+ const normalizedMimeType = normalizeMimeType(candidate.mimeType) || inferMimeType(name, kind);
153
+ return {
154
+ ok: true,
155
+ kind,
156
+ name,
157
+ mimeType: normalizedMimeType,
158
+ sizeBytes: candidate.sizeBytes,
159
+ };
160
+ }
161
+
162
+ export function normalizeChatAttachments(input: unknown): ChatAttachment[] {
163
+ if (!Array.isArray(input)) {
164
+ return [];
165
+ }
166
+
167
+ const normalized: ChatAttachment[] = [];
168
+ const seen = new Set<string>();
169
+
170
+ for (const item of input) {
171
+ if (!item || typeof item !== "object") {
172
+ continue;
173
+ }
174
+ const record = item as {
175
+ provider?: unknown;
176
+ providerFileId?: unknown;
177
+ kind?: unknown;
178
+ name?: unknown;
179
+ mimeType?: unknown;
180
+ sizeBytes?: unknown;
181
+ };
182
+
183
+ const provider =
184
+ record.provider === "openai" || record.provider === "anthropic"
185
+ ? record.provider
186
+ : null;
187
+ const providerFileId =
188
+ typeof record.providerFileId === "string" ? record.providerFileId.trim() : "";
189
+ const kind =
190
+ record.kind === "image" || record.kind === "pdf" || record.kind === "document"
191
+ ? record.kind
192
+ : null;
193
+ const name = typeof record.name === "string" ? normalizeFileName(record.name) : "";
194
+ const mimeType = normalizeMimeType(
195
+ typeof record.mimeType === "string" ? record.mimeType : "",
196
+ );
197
+ const sizeBytes =
198
+ typeof record.sizeBytes === "number" && Number.isFinite(record.sizeBytes) && record.sizeBytes > 0
199
+ ? record.sizeBytes
200
+ : 0;
201
+
202
+ if (!provider || !providerFileId || !kind || !name || !mimeType || sizeBytes <= 0) {
203
+ continue;
204
+ }
205
+
206
+ const uniqueKey = `${provider}:${providerFileId}`;
207
+ if (seen.has(uniqueKey)) {
208
+ continue;
209
+ }
210
+ seen.add(uniqueKey);
211
+ normalized.push({
212
+ provider,
213
+ providerFileId,
214
+ kind,
215
+ name,
216
+ mimeType,
217
+ sizeBytes,
218
+ });
219
+ }
220
+
221
+ return normalized;
222
+ }
@@ -1,6 +1,7 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { db } from "./db";
3
3
  import type {
4
+ ChatAttachment,
4
5
  LocalToolsSettings,
5
6
  MemorySettings,
6
7
  MessageNode,
@@ -292,10 +293,11 @@ export async function deleteConversation(conversationId: string) {
292
293
  export async function appendUserAndAssistant(params: {
293
294
  thread: Thread;
294
295
  content: string;
296
+ attachments?: ChatAttachment[];
295
297
  provider: string;
296
298
  model: string;
297
299
  }) {
298
- const { thread, content, provider, model } = params;
300
+ const { thread, content, attachments, provider, model } = params;
299
301
  const now = Date.now();
300
302
  const userId = nanoid();
301
303
  const assistantId = nanoid();
@@ -306,6 +308,7 @@ export async function appendUserAndAssistant(params: {
306
308
  parentId: thread.headMessageId,
307
309
  role: "user",
308
310
  content,
311
+ ...(attachments?.length ? { attachments } : {}),
309
312
  createdAt: now,
310
313
  };
311
314
 
@@ -29,6 +29,7 @@ export type MessageNode = {
29
29
  parentId: string | null;
30
30
  role: "system" | "user" | "assistant";
31
31
  content: string;
32
+ attachments?: ChatAttachment[];
32
33
  createdAt: number;
33
34
  provider?: string;
34
35
  model?: string;
@@ -217,6 +218,16 @@ export type ToolApproval = {
217
218
  export type ChatMessageInput = {
218
219
  role: "user" | "assistant" | "system";
219
220
  content: string;
221
+ attachments?: ChatAttachment[];
222
+ };
223
+
224
+ export type ChatAttachment = {
225
+ provider: "openai" | "anthropic";
226
+ providerFileId: string;
227
+ kind: "image" | "pdf" | "document";
228
+ name: string;
229
+ mimeType: string;
230
+ sizeBytes: number;
220
231
  };
221
232
 
222
233
  export type ChatCitationSource = {