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.
- package/package.json +1 -1
- package/template/next-env.d.ts +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +321 -23
- package/template/src/app/api/chat/uploads/route.ts +220 -0
- package/template/src/app/globals.css +105 -3
- package/template/src/components/ChatView.tsx +210 -5
- package/template/src/components/Composer.tsx +93 -3
- package/template/src/components/MessageCard.tsx +28 -1
- package/template/src/lib/attachments.ts +222 -0
- package/template/src/lib/data.ts +4 -1
- package/template/src/lib/types.ts +11 -0
|
@@ -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
|
+
}
|
package/template/src/lib/data.ts
CHANGED
|
@@ -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 = {
|