@boldare/askdare 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/cli.js +1461 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +193 -0
- package/dist/next.d.ts +86 -0
- package/dist/next.js +2864 -0
- package/dist/store.d.ts +413 -0
- package/dist/version.js +641 -0
- package/docs/attachments.md +41 -0
- package/docs/cli.md +227 -0
- package/docs/configuration.md +357 -0
- package/docs/costs.md +181 -0
- package/docs/custom-store.md +259 -0
- package/docs/deployment.md +76 -0
- package/docs/embedding.md +258 -0
- package/docs/getting-started.md +116 -0
- package/docs/going-public.md +81 -0
- package/docs/identity.md +109 -0
- package/docs/knowledge-base.md +65 -0
- package/docs/migrating-from-openai-assistant.md +43 -0
- package/docs/privacy.md +175 -0
- package/docs/protocol.md +174 -0
- package/docs/styling.md +166 -0
- package/docs/support.md +48 -0
- package/docs/texts.md +176 -0
- package/docs/themes.md +172 -0
- package/docs/tools.md +52 -0
- package/docs/troubleshooting.md +345 -0
- package/docs/voice-input.md +31 -0
- package/llms.txt +32 -0
- package/package.json +98 -3
- package/skills/askdare/SKILL.md +36 -0
package/dist/version.js
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// src/i18n/texts.ts
|
|
3
|
+
/**
|
|
4
|
+
* Every text the widget shows. The keys are public API (ADR-0008): override any of them with
|
|
5
|
+
* `widget.texts`. Plural keys end in a CLDR category (`zero`, `one`, `two`, `few`, `many`, `other`).
|
|
6
|
+
*/
|
|
7
|
+
const EN = {
|
|
8
|
+
"header.title": "Assistant",
|
|
9
|
+
"header.newConversation": "Start a new conversation",
|
|
10
|
+
"header.close": "Close chat",
|
|
11
|
+
"confirm.newConversation": "Start a new conversation? The current one will be deleted.",
|
|
12
|
+
"confirm.cancel": "Cancel",
|
|
13
|
+
"confirm.accept": "Start new",
|
|
14
|
+
"welcome.label": "Welcome",
|
|
15
|
+
"welcome.greeting": "Hi! How can I help?",
|
|
16
|
+
"welcome.online": "Online",
|
|
17
|
+
"welcome.suggestions": "Suggested questions",
|
|
18
|
+
"composer.placeholder": "Type your message…",
|
|
19
|
+
"composer.send": "Send",
|
|
20
|
+
"composer.attach": "Attach files",
|
|
21
|
+
"composer.dictate": "Dictate a message",
|
|
22
|
+
"status.loading": "Loading the conversation…",
|
|
23
|
+
"status.thinking": "Thinking…",
|
|
24
|
+
"status.searching": "Searching documents…",
|
|
25
|
+
"status.tool": "Using {name}…",
|
|
26
|
+
"status.typing": "The assistant is typing",
|
|
27
|
+
"message.sources": "Sources",
|
|
28
|
+
"message.attachments.one": "{count} attachment",
|
|
29
|
+
"message.attachments.other": "{count} attachments",
|
|
30
|
+
"attachment.list": "Attachments",
|
|
31
|
+
"attachment.uploading": "Uploading…",
|
|
32
|
+
"attachment.remove": "Remove {name}",
|
|
33
|
+
"attachment.unsupported": "This file type is not supported.",
|
|
34
|
+
"attachment.tooLarge": "Too large (max {size}).",
|
|
35
|
+
"attachment.tooMany": "You can attach up to {count} files per message.",
|
|
36
|
+
"dictation.listening": "Listening…",
|
|
37
|
+
"dictation.transcribing": "Transcribing…",
|
|
38
|
+
"dictation.cancel": "Cancel dictation",
|
|
39
|
+
"dictation.done": "Done",
|
|
40
|
+
"dictation.empty": "Nothing was recorded. Please try again.",
|
|
41
|
+
"microphone.blocked": "Microphone access is blocked. Allow it in your browser to dictate.",
|
|
42
|
+
"microphone.missing": "No microphone was found.",
|
|
43
|
+
"microphone.failed": "The microphone could not be started.",
|
|
44
|
+
"disclosure.text": "You’re chatting with an AI assistant. It can make mistakes.",
|
|
45
|
+
"disclosure.privacy": "Privacy policy",
|
|
46
|
+
"launcher.label": "Ask us",
|
|
47
|
+
"launcher.open": "Open chat",
|
|
48
|
+
"launcher.close": "Close chat",
|
|
49
|
+
"launcher.unavailable": "The chat couldn’t load. Please try again later.",
|
|
50
|
+
"error.connection": "Connection problem. Please try again.",
|
|
51
|
+
"error.history": "Could not load the previous conversation.",
|
|
52
|
+
"error.updated": "The assistant was updated. Please reload the page.",
|
|
53
|
+
"error.stalled": "The connection stalled. Please try again.",
|
|
54
|
+
"error.uploadFailed": "The file could not be uploaded.",
|
|
55
|
+
"error.transcriptionFailed": "The recording could not be transcribed.",
|
|
56
|
+
"error.invalid_input": "The request was invalid.",
|
|
57
|
+
"error.unauthorized": "Authentication is required.",
|
|
58
|
+
"error.session_expired": "Your session has expired. Please try again.",
|
|
59
|
+
"error.not_found": "Not found.",
|
|
60
|
+
"error.content_blocked": "This message cannot be processed.",
|
|
61
|
+
"error.rate_limited": "Too many messages. Please wait a moment.",
|
|
62
|
+
"error.budget_exceeded": "The assistant is unavailable for the rest of the day.",
|
|
63
|
+
"error.internal_error": "Something went wrong. Please try again.",
|
|
64
|
+
"error.upstream_error": "The assistant is temporarily unavailable. Please try again.",
|
|
65
|
+
"error.bot_unavailable": "The assistant is currently unavailable.",
|
|
66
|
+
"error.timeout": "The assistant took too long to respond. Please try again.",
|
|
67
|
+
"error.conversationTooLong": "This conversation is too long. Please start a new one.",
|
|
68
|
+
"error.conversationGone": "This conversation is no longer available.",
|
|
69
|
+
"error.messageLength": "Messages can be up to {max} characters.",
|
|
70
|
+
"error.attachmentExpired": "An attachment expired or is invalid. Please attach it again.",
|
|
71
|
+
"error.filesNotAccepted": "This assistant does not accept files.",
|
|
72
|
+
"error.tooManyFiles": "Attach up to {count} files per message.",
|
|
73
|
+
"error.fileType.images": "Only images (PNG, JPEG, WebP, GIF) are supported.",
|
|
74
|
+
"error.fileType.pdf": "Only PDF files are supported.",
|
|
75
|
+
"error.fileType.both": "Only images (PNG, JPEG, WebP, GIF) and PDF files are supported.",
|
|
76
|
+
"error.fileTooLarge": "Files can be up to {size}.",
|
|
77
|
+
"error.pdfPages": "PDFs can have up to {max} pages.",
|
|
78
|
+
"error.voiceNotAccepted": "This assistant does not accept voice input.",
|
|
79
|
+
"error.recordingFormat": "This recording format is not supported.",
|
|
80
|
+
"error.recordingTooLong": "This recording is too long.",
|
|
81
|
+
"error.dictationBusy": "Another dictation is still being transcribed. Please try again in a moment.",
|
|
82
|
+
"error.dictationBudget": "Dictation is unavailable for the rest of the day. You can still type."
|
|
83
|
+
};
|
|
84
|
+
/** Plural texts: the base plus a CLDR category, every category overridable. */
|
|
85
|
+
const PLURAL_BASES = ["message.attachments"];
|
|
86
|
+
const PLURAL_FORMS = [
|
|
87
|
+
"zero",
|
|
88
|
+
"one",
|
|
89
|
+
"two",
|
|
90
|
+
"few",
|
|
91
|
+
"many",
|
|
92
|
+
"other"
|
|
93
|
+
];
|
|
94
|
+
const PL = {
|
|
95
|
+
"header.title": "Asystent",
|
|
96
|
+
"header.newConversation": "Zacznij nową rozmowę",
|
|
97
|
+
"header.close": "Zamknij czat",
|
|
98
|
+
"confirm.newConversation": "Zacząć nową rozmowę? Obecna zostanie usunięta.",
|
|
99
|
+
"confirm.cancel": "Anuluj",
|
|
100
|
+
"confirm.accept": "Zacznij nową",
|
|
101
|
+
"welcome.label": "Powitanie",
|
|
102
|
+
"welcome.greeting": "Cześć! W czym mogę pomóc?",
|
|
103
|
+
"welcome.online": "Online",
|
|
104
|
+
"welcome.suggestions": "Proponowane pytania",
|
|
105
|
+
"composer.placeholder": "Napisz wiadomość…",
|
|
106
|
+
"composer.send": "Wyślij",
|
|
107
|
+
"composer.attach": "Dołącz pliki",
|
|
108
|
+
"composer.dictate": "Podyktuj wiadomość",
|
|
109
|
+
"status.loading": "Wczytuję rozmowę…",
|
|
110
|
+
"status.thinking": "Myślę…",
|
|
111
|
+
"status.searching": "Przeszukuję dokumenty…",
|
|
112
|
+
"status.tool": "Korzystam z narzędzia {name}…",
|
|
113
|
+
"status.typing": "Asystent pisze",
|
|
114
|
+
"message.sources": "Źródła",
|
|
115
|
+
"message.attachments.one": "{count} załącznik",
|
|
116
|
+
"message.attachments.few": "{count} załączniki",
|
|
117
|
+
"message.attachments.many": "{count} załączników",
|
|
118
|
+
"message.attachments.other": "{count} załącznika",
|
|
119
|
+
"attachment.list": "Załączniki",
|
|
120
|
+
"attachment.uploading": "Wysyłam…",
|
|
121
|
+
"attachment.remove": "Usuń {name}",
|
|
122
|
+
"attachment.unsupported": "Ten typ pliku nie jest obsługiwany.",
|
|
123
|
+
"attachment.tooLarge": "Za duży (maks. {size}).",
|
|
124
|
+
"attachment.tooMany": "Limit plików w jednej wiadomości: {count}.",
|
|
125
|
+
"dictation.listening": "Słucham…",
|
|
126
|
+
"dictation.transcribing": "Przepisuję…",
|
|
127
|
+
"dictation.cancel": "Anuluj dyktowanie",
|
|
128
|
+
"dictation.done": "Gotowe",
|
|
129
|
+
"dictation.empty": "Nic się nie nagrało. Spróbuj ponownie.",
|
|
130
|
+
"microphone.blocked": "Dostęp do mikrofonu jest zablokowany. Zezwól na niego w przeglądarce.",
|
|
131
|
+
"microphone.missing": "Nie znaleziono mikrofonu.",
|
|
132
|
+
"microphone.failed": "Nie udało się włączyć mikrofonu.",
|
|
133
|
+
"disclosure.text": "Rozmawiasz z asystentem AI. Może popełniać błędy.",
|
|
134
|
+
"disclosure.privacy": "Polityka prywatności",
|
|
135
|
+
"launcher.label": "Zapytaj nas",
|
|
136
|
+
"launcher.open": "Otwórz czat",
|
|
137
|
+
"launcher.close": "Zamknij czat",
|
|
138
|
+
"launcher.unavailable": "Nie udało się załadować czatu. Spróbuj ponownie później.",
|
|
139
|
+
"error.connection": "Problem z połączeniem. Spróbuj ponownie.",
|
|
140
|
+
"error.history": "Nie udało się wczytać poprzedniej rozmowy.",
|
|
141
|
+
"error.updated": "Asystent został zaktualizowany. Odśwież stronę.",
|
|
142
|
+
"error.stalled": "Połączenie zostało przerwane. Spróbuj ponownie.",
|
|
143
|
+
"error.uploadFailed": "Nie udało się wysłać pliku.",
|
|
144
|
+
"error.transcriptionFailed": "Nie udało się przepisać nagrania.",
|
|
145
|
+
"error.invalid_input": "Nieprawidłowe żądanie.",
|
|
146
|
+
"error.unauthorized": "Wymagane jest uwierzytelnienie.",
|
|
147
|
+
"error.session_expired": "Sesja wygasła. Spróbuj ponownie.",
|
|
148
|
+
"error.not_found": "Nie znaleziono.",
|
|
149
|
+
"error.content_blocked": "Nie możemy przetworzyć tej wiadomości.",
|
|
150
|
+
"error.rate_limited": "Zbyt wiele wiadomości. Poczekaj chwilę.",
|
|
151
|
+
"error.budget_exceeded": "Asystent jest niedostępny do końca dnia.",
|
|
152
|
+
"error.internal_error": "Coś poszło nie tak. Spróbuj ponownie.",
|
|
153
|
+
"error.upstream_error": "Asystent jest chwilowo niedostępny. Spróbuj ponownie.",
|
|
154
|
+
"error.bot_unavailable": "Asystent jest obecnie niedostępny.",
|
|
155
|
+
"error.timeout": "Odpowiedź trwała zbyt długo. Spróbuj ponownie.",
|
|
156
|
+
"error.conversationTooLong": "Ta rozmowa jest zbyt długa. Zacznij nową.",
|
|
157
|
+
"error.conversationGone": "Ta rozmowa nie jest już dostępna.",
|
|
158
|
+
"error.messageLength": "Wiadomość może mieć maksymalnie {max} znaków.",
|
|
159
|
+
"error.attachmentExpired": "Załącznik wygasł lub jest nieprawidłowy. Dołącz go ponownie.",
|
|
160
|
+
"error.filesNotAccepted": "Ten asystent nie przyjmuje plików.",
|
|
161
|
+
"error.tooManyFiles": "Limit plików w jednej wiadomości: {count}.",
|
|
162
|
+
"error.fileType.images": "Obsługujemy tylko obrazy (PNG, JPEG, WebP, GIF).",
|
|
163
|
+
"error.fileType.pdf": "Obsługujemy tylko pliki PDF.",
|
|
164
|
+
"error.fileType.both": "Obsługujemy tylko obrazy (PNG, JPEG, WebP, GIF) i pliki PDF.",
|
|
165
|
+
"error.fileTooLarge": "Plik może mieć maksymalnie {size}.",
|
|
166
|
+
"error.pdfPages": "PDF może mieć maksymalnie {max} stron.",
|
|
167
|
+
"error.voiceNotAccepted": "Ten asystent nie przyjmuje nagrań głosowych.",
|
|
168
|
+
"error.recordingFormat": "Ten format nagrania nie jest obsługiwany.",
|
|
169
|
+
"error.recordingTooLong": "To nagranie jest zbyt długie.",
|
|
170
|
+
"error.dictationBusy": "Poprzednie nagranie jest jeszcze przepisywane. Spróbuj za chwilę.",
|
|
171
|
+
"error.dictationBudget": "Dyktowanie jest niedostępne do końca dnia. Wiadomość możesz nadal wpisać."
|
|
172
|
+
};
|
|
173
|
+
/** The canonical form of a BCP 47 tag that Intl accepts (plural rules need it), else undefined. */
|
|
174
|
+
function canonicalLocale(tag) {
|
|
175
|
+
if (tag.length > 35) return void 0;
|
|
176
|
+
try {
|
|
177
|
+
return Intl.getCanonicalLocales(tag)[0];
|
|
178
|
+
} catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Languages with a built-in dictionary; any other locale shows English texts. */
|
|
183
|
+
const TEXT_LOCALES = ["en", "pl"];
|
|
184
|
+
const DICTIONARIES = {
|
|
185
|
+
en: EN,
|
|
186
|
+
pl: {
|
|
187
|
+
...EN,
|
|
188
|
+
...PL
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
/** The dictionary language for a BCP 47 tag: its primary subtag when built in, else English. */
|
|
192
|
+
function textLocale(locale) {
|
|
193
|
+
const primary = locale.toLowerCase().split("-")[0];
|
|
194
|
+
return TEXT_LOCALES.includes(primary) ? primary : "en";
|
|
195
|
+
}
|
|
196
|
+
/** Every key `widget.texts` accepts. */
|
|
197
|
+
const TEXT_KEYS = [.../* @__PURE__ */ new Set([...Object.keys(EN), ...PLURAL_BASES.flatMap((base) => PLURAL_FORMS.map((form) => `${base}.${form}`))])];
|
|
198
|
+
/**
|
|
199
|
+
* The full dictionary for a locale with overrides applied. Plural forms beyond `one`/`other`
|
|
200
|
+
* (Polish `few`, `many`) are kept too, so the widget can pick them.
|
|
201
|
+
*/
|
|
202
|
+
function textsFor(locale, overrides = {}) {
|
|
203
|
+
return {
|
|
204
|
+
...DICTIONARIES[textLocale(locale)],
|
|
205
|
+
...overrides
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// src/theme/presets.ts
|
|
209
|
+
const PRESET_NAMES = [
|
|
210
|
+
"halo",
|
|
211
|
+
"aura",
|
|
212
|
+
"sage",
|
|
213
|
+
"care"
|
|
214
|
+
];
|
|
215
|
+
const SYSTEM = "system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif";
|
|
216
|
+
const GEIST = `"Geist Variable", ${SYSTEM}`;
|
|
217
|
+
/**
|
|
218
|
+
* The four built-in looks. Inspirations set the mood only; the designs are original (ADR-0008).
|
|
219
|
+
* Every pair a reader sees is checked for contrast in `contrast.test.ts`.
|
|
220
|
+
*/
|
|
221
|
+
const PRESETS = {
|
|
222
|
+
/** Clean and friendly: white into ice blue, a frosted orb in rings of light, a bold question. */
|
|
223
|
+
halo: {
|
|
224
|
+
light: {
|
|
225
|
+
accent: "#1f5fe0",
|
|
226
|
+
onAccent: "#ffffff",
|
|
227
|
+
background: "#ffffff",
|
|
228
|
+
surface: "#eef4fd",
|
|
229
|
+
text: "#0d1526",
|
|
230
|
+
muted: "#4f5b72",
|
|
231
|
+
border: "#0d15261a",
|
|
232
|
+
danger: "#b42318",
|
|
233
|
+
success: "#067647",
|
|
234
|
+
userBubble: "#dce9fc",
|
|
235
|
+
onUserBubble: "#0d1526",
|
|
236
|
+
backdrop: "radial-gradient(120% 55% at 50% -10%, #d3e4ff 0%, #ffffff00 65%), linear-gradient(180deg, #f3f8ff 0%, #ffffff 55%, #eef5ff 100%)",
|
|
237
|
+
orb: "radial-gradient(circle at 36% 28%, #ffffff 0%, #eef5ff 14%, #b4d0ff 44%, #5b8ff2 76%, #2a5fd6 100%)",
|
|
238
|
+
fontFamily: GEIST,
|
|
239
|
+
headingFamily: GEIST,
|
|
240
|
+
fontSize: "15px",
|
|
241
|
+
radius: "18px"
|
|
242
|
+
},
|
|
243
|
+
dark: {
|
|
244
|
+
accent: "#82adff",
|
|
245
|
+
onAccent: "#06122b",
|
|
246
|
+
background: "#0a0f1c",
|
|
247
|
+
surface: "#151d30",
|
|
248
|
+
text: "#e9eef8",
|
|
249
|
+
muted: "#a0acc3",
|
|
250
|
+
border: "#ffffff1c",
|
|
251
|
+
danger: "#ff9c8f",
|
|
252
|
+
success: "#5fdc8f",
|
|
253
|
+
userBubble: "#1d3566",
|
|
254
|
+
onUserBubble: "#e9eef8",
|
|
255
|
+
backdrop: "radial-gradient(120% 55% at 50% -10%, #1a2b55 0%, #0a0f1c00 65%), linear-gradient(180deg, #0d1528 0%, #0a0f1c 60%)",
|
|
256
|
+
orb: "radial-gradient(circle at 36% 28%, #ffffff 0%, #d6e5ff 14%, #86aefc 44%, #3466de 76%, #173c9c 100%)",
|
|
257
|
+
fontFamily: GEIST,
|
|
258
|
+
headingFamily: GEIST,
|
|
259
|
+
fontSize: "15px",
|
|
260
|
+
radius: "18px"
|
|
261
|
+
},
|
|
262
|
+
variants: {
|
|
263
|
+
avatar: "orb",
|
|
264
|
+
welcome: "hero",
|
|
265
|
+
launcher: "orb",
|
|
266
|
+
bubbles: "filled",
|
|
267
|
+
surface: "glass",
|
|
268
|
+
density: "comfortable",
|
|
269
|
+
composer: "outline"
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
/**
|
|
273
|
+
* Soft glass for AI products: an airy pastel field, a glossy bubble with its reflection, a
|
|
274
|
+
* two-weight headline. Colors from Boldare's palette: coral, violet and teal.
|
|
275
|
+
*/
|
|
276
|
+
aura: {
|
|
277
|
+
light: {
|
|
278
|
+
accent: "#6b52e0",
|
|
279
|
+
onAccent: "#ffffff",
|
|
280
|
+
background: "#fdfbfd",
|
|
281
|
+
surface: "#f5f0f7",
|
|
282
|
+
text: "#242424",
|
|
283
|
+
muted: "#5f5a68",
|
|
284
|
+
border: "#2424241a",
|
|
285
|
+
danger: "#b42318",
|
|
286
|
+
success: "#067647",
|
|
287
|
+
userBubble: "#c2386f",
|
|
288
|
+
onUserBubble: "#ffffff",
|
|
289
|
+
backdrop: "radial-gradient(60% 45% at 6% 0%, #ffdbe7 0%, #ffdbe700 70%), radial-gradient(55% 40% at 100% 6%, #e4ddff 0%, #e4ddff00 70%), radial-gradient(70% 45% at 60% 106%, #d2f3ec 0%, #d2f3ec00 70%), linear-gradient(#fdfbfd, #fdfbfd)",
|
|
290
|
+
orb: "radial-gradient(circle at 33% 24%, #ffffff 0%, #ffffffd9 6%, #ffffff00 19%), radial-gradient(circle at 70% 84%, #9df3e6 0%, #9df3e600 44%), radial-gradient(circle at 50% 28%, #ffa3c6 0%, #f28bbb 24%, #b8a1f6 60%, #7dd8d0 100%)",
|
|
291
|
+
fontFamily: `"Plus Jakarta Sans Variable", ${SYSTEM}`,
|
|
292
|
+
headingFamily: `"Plus Jakarta Sans Variable", ${SYSTEM}`,
|
|
293
|
+
fontSize: "15px",
|
|
294
|
+
radius: "22px"
|
|
295
|
+
},
|
|
296
|
+
dark: {
|
|
297
|
+
accent: "#b9a8ff",
|
|
298
|
+
onAccent: "#1b1530",
|
|
299
|
+
background: "#17141c",
|
|
300
|
+
surface: "#242029",
|
|
301
|
+
text: "#f4f1f8",
|
|
302
|
+
muted: "#b8b0c4",
|
|
303
|
+
border: "#ffffff1c",
|
|
304
|
+
danger: "#ff9c8f",
|
|
305
|
+
success: "#5fdc8f",
|
|
306
|
+
userBubble: "#a52f63",
|
|
307
|
+
onUserBubble: "#ffffff",
|
|
308
|
+
backdrop: "radial-gradient(60% 45% at 6% 0%, #4a1f36 0%, #4a1f3600 70%), radial-gradient(55% 40% at 100% 6%, #2c2457 0%, #2c245700 70%), radial-gradient(70% 45% at 60% 106%, #133b37 0%, #133b3700 70%), linear-gradient(#17141c, #17141c)",
|
|
309
|
+
orb: "radial-gradient(circle at 33% 24%, #ffffff 0%, #ffffffc4 6%, #ffffff00 19%), radial-gradient(circle at 70% 84%, #6fe3d2 0%, #6fe3d200 44%), radial-gradient(circle at 50% 28%, #ff8fb9 0%, #e173aa 24%, #9a83ec 60%, #4fc1b7 100%)",
|
|
310
|
+
fontFamily: `"Plus Jakarta Sans Variable", ${SYSTEM}`,
|
|
311
|
+
headingFamily: `"Plus Jakarta Sans Variable", ${SYSTEM}`,
|
|
312
|
+
fontSize: "15px",
|
|
313
|
+
radius: "22px"
|
|
314
|
+
},
|
|
315
|
+
variants: {
|
|
316
|
+
avatar: "orb",
|
|
317
|
+
welcome: "hero",
|
|
318
|
+
launcher: "orb",
|
|
319
|
+
bubbles: "filled",
|
|
320
|
+
surface: "glass",
|
|
321
|
+
density: "comfortable",
|
|
322
|
+
composer: "gradient"
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
/** A calm companion: sage and mint, light type, a persona card, black pill buttons. */
|
|
326
|
+
sage: {
|
|
327
|
+
light: {
|
|
328
|
+
accent: "#111311",
|
|
329
|
+
onAccent: "#f5f6f2",
|
|
330
|
+
background: "#f2f3ef",
|
|
331
|
+
surface: "#dde8d3",
|
|
332
|
+
text: "#1a201c",
|
|
333
|
+
muted: "#535b55",
|
|
334
|
+
border: "#1a201c17",
|
|
335
|
+
danger: "#a8261b",
|
|
336
|
+
success: "#2f6b3a",
|
|
337
|
+
userBubble: "#ffffff",
|
|
338
|
+
onUserBubble: "#1a201c",
|
|
339
|
+
backdrop: "radial-gradient(80% 50% at 85% 0%, #e0ebd6 0%, #e0ebd600 70%), linear-gradient(180deg, #eef0ea 0%, #f2f3ef 50%)",
|
|
340
|
+
orb: "radial-gradient(circle at 34% 28%, #ffffff 0%, #e6eedf 26%, #a9c29b 62%, #6d8c62 100%)",
|
|
341
|
+
fontFamily: GEIST,
|
|
342
|
+
headingFamily: GEIST,
|
|
343
|
+
fontSize: "15px",
|
|
344
|
+
radius: "20px"
|
|
345
|
+
},
|
|
346
|
+
dark: {
|
|
347
|
+
accent: "#eef0ea",
|
|
348
|
+
onAccent: "#121512",
|
|
349
|
+
background: "#131613",
|
|
350
|
+
surface: "#26321f",
|
|
351
|
+
text: "#eceee8",
|
|
352
|
+
muted: "#a9b3ab",
|
|
353
|
+
border: "#ffffff1c",
|
|
354
|
+
danger: "#ff9c8f",
|
|
355
|
+
success: "#8fd19b",
|
|
356
|
+
userBubble: "#2a2f2a",
|
|
357
|
+
onUserBubble: "#eceee8",
|
|
358
|
+
backdrop: "radial-gradient(80% 50% at 85% 0%, #1f2a1b 0%, #1f2a1b00 70%), linear-gradient(180deg, #171b16 0%, #131613 50%)",
|
|
359
|
+
orb: "radial-gradient(circle at 34% 28%, #f2f5ee 0%, #c3d3b8 28%, #7c9a70 64%, #3b5137 100%)",
|
|
360
|
+
fontFamily: GEIST,
|
|
361
|
+
headingFamily: GEIST,
|
|
362
|
+
fontSize: "15px",
|
|
363
|
+
radius: "20px"
|
|
364
|
+
},
|
|
365
|
+
variants: {
|
|
366
|
+
avatar: "orb",
|
|
367
|
+
welcome: "persona",
|
|
368
|
+
launcher: "pill",
|
|
369
|
+
bubbles: "filled",
|
|
370
|
+
surface: "solid",
|
|
371
|
+
density: "comfortable",
|
|
372
|
+
composer: "outline"
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
/** Trust first: indigo and lavender, a capability checklist, hyperlegible type, AAA text. */
|
|
376
|
+
care: {
|
|
377
|
+
light: {
|
|
378
|
+
accent: "#3730a3",
|
|
379
|
+
onAccent: "#ffffff",
|
|
380
|
+
background: "#ffffff",
|
|
381
|
+
surface: "#f0eeff",
|
|
382
|
+
text: "#111827",
|
|
383
|
+
muted: "#3d4455",
|
|
384
|
+
border: "#11182726",
|
|
385
|
+
danger: "#97170e",
|
|
386
|
+
success: "#05603a",
|
|
387
|
+
userBubble: "#3730a3",
|
|
388
|
+
onUserBubble: "#ffffff",
|
|
389
|
+
backdrop: "none",
|
|
390
|
+
orb: "radial-gradient(circle at 34% 28%, #ffffff 0%, #d9d4ff 30%, #7b72e8 66%, #3730a3 100%)",
|
|
391
|
+
fontFamily: `"Atkinson Hyperlegible Next Variable", ${SYSTEM}`,
|
|
392
|
+
headingFamily: `"Atkinson Hyperlegible Next Variable", ${SYSTEM}`,
|
|
393
|
+
fontSize: "16px",
|
|
394
|
+
radius: "14px"
|
|
395
|
+
},
|
|
396
|
+
dark: {
|
|
397
|
+
accent: "#c9d0ff",
|
|
398
|
+
onAccent: "#0e1020",
|
|
399
|
+
background: "#0e1020",
|
|
400
|
+
surface: "#1c1f3a",
|
|
401
|
+
text: "#f5f6fb",
|
|
402
|
+
muted: "#ccd0e4",
|
|
403
|
+
border: "#ffffff26",
|
|
404
|
+
danger: "#ffc2b8",
|
|
405
|
+
success: "#86efbd",
|
|
406
|
+
userBubble: "#3730a3",
|
|
407
|
+
onUserBubble: "#ffffff",
|
|
408
|
+
backdrop: "none",
|
|
409
|
+
orb: "radial-gradient(circle at 34% 28%, #ffffff 0%, #cfc9ff 30%, #7b72e8 66%, #3730a3 100%)",
|
|
410
|
+
fontFamily: `"Atkinson Hyperlegible Next Variable", ${SYSTEM}`,
|
|
411
|
+
headingFamily: `"Atkinson Hyperlegible Next Variable", ${SYSTEM}`,
|
|
412
|
+
fontSize: "16px",
|
|
413
|
+
radius: "14px"
|
|
414
|
+
},
|
|
415
|
+
variants: {
|
|
416
|
+
avatar: "icon",
|
|
417
|
+
welcome: "intro",
|
|
418
|
+
launcher: "pill",
|
|
419
|
+
bubbles: "filled",
|
|
420
|
+
surface: "solid",
|
|
421
|
+
density: "comfortable",
|
|
422
|
+
composer: "outline"
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
// src/theme/contrast.ts
|
|
427
|
+
const HEX = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i;
|
|
428
|
+
function channels(hex) {
|
|
429
|
+
const match = HEX.exec(hex);
|
|
430
|
+
if (!match?.[1]) throw new TypeError(`Not a hex color: ${hex}`);
|
|
431
|
+
const value = Number.parseInt(match[1], 16);
|
|
432
|
+
return [
|
|
433
|
+
value >> 16 & 255,
|
|
434
|
+
value >> 8 & 255,
|
|
435
|
+
value & 255
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
function luminance(hex) {
|
|
439
|
+
const [red, green, blue] = channels(hex).map((channel) => {
|
|
440
|
+
const c = channel / 255;
|
|
441
|
+
return c <= .03928 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
442
|
+
});
|
|
443
|
+
return .2126 * red + .7152 * green + .0722 * blue;
|
|
444
|
+
}
|
|
445
|
+
/** WCAG 2.x contrast ratio between two opaque hex colors, from 1 to 21. */
|
|
446
|
+
function contrastRatio(foreground, background) {
|
|
447
|
+
const [light, dark] = [luminance(foreground), luminance(background)].sort((a, b) => b - a);
|
|
448
|
+
return (light + .05) / (dark + .05);
|
|
449
|
+
}
|
|
450
|
+
/** Black or white, whichever reads better on the given color. */
|
|
451
|
+
function readableOn(background) {
|
|
452
|
+
return contrastRatio("#ffffff", background) >= contrastRatio("#0b0b0b", background) ? "#ffffff" : "#0b0b0b";
|
|
453
|
+
}
|
|
454
|
+
/** Every pairing a visitor reads in the widget. */
|
|
455
|
+
const CONTRAST_PAIRS = [
|
|
456
|
+
{
|
|
457
|
+
name: "text on panel",
|
|
458
|
+
foreground: "text",
|
|
459
|
+
background: "background",
|
|
460
|
+
kind: "text"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "text in bubbles",
|
|
464
|
+
foreground: "text",
|
|
465
|
+
background: "surface",
|
|
466
|
+
kind: "text"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: "secondary text on panel",
|
|
470
|
+
foreground: "muted",
|
|
471
|
+
background: "background",
|
|
472
|
+
kind: "text"
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "secondary text in cards",
|
|
476
|
+
foreground: "muted",
|
|
477
|
+
background: "surface",
|
|
478
|
+
kind: "text"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "button labels",
|
|
482
|
+
foreground: "onAccent",
|
|
483
|
+
background: "accent",
|
|
484
|
+
kind: "text"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "own messages",
|
|
488
|
+
foreground: "onUserBubble",
|
|
489
|
+
background: "userBubble",
|
|
490
|
+
kind: "text"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "links in bubbles",
|
|
494
|
+
foreground: "accent",
|
|
495
|
+
background: "surface",
|
|
496
|
+
kind: "text"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "links on panel",
|
|
500
|
+
foreground: "accent",
|
|
501
|
+
background: "background",
|
|
502
|
+
kind: "text"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "errors",
|
|
506
|
+
foreground: "danger",
|
|
507
|
+
background: "background",
|
|
508
|
+
kind: "text"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "checks",
|
|
512
|
+
foreground: "success",
|
|
513
|
+
background: "surface",
|
|
514
|
+
kind: "graphic"
|
|
515
|
+
}
|
|
516
|
+
];
|
|
517
|
+
/** Pairs below WCAG AA, or AAA for text when `level` is 'AAA'. */
|
|
518
|
+
function contrastFailures(tokens, level = "AA") {
|
|
519
|
+
return CONTRAST_PAIRS.flatMap((pair) => {
|
|
520
|
+
const required = pair.kind === "graphic" ? 3 : level === "AAA" ? 7 : 4.5;
|
|
521
|
+
const ratio = contrastRatio(tokens[pair.foreground], tokens[pair.background]);
|
|
522
|
+
return ratio >= required ? [] : [{
|
|
523
|
+
name: pair.name,
|
|
524
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
525
|
+
required
|
|
526
|
+
}];
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// src/theme/theme.ts
|
|
530
|
+
const color = z.string().regex(/^#[0-9a-fA-F]{6}$/, "must be a hex color like #1f5fe0");
|
|
531
|
+
/**
|
|
532
|
+
* Plain CSS values only: they are written into a stylesheet, and the orb into host pages. No
|
|
533
|
+
* quotes, so no image-set("https://…") or src("…") can make a browser fetch from another origin.
|
|
534
|
+
*/
|
|
535
|
+
const cssValue = z.string().max(600).refine((value) => !/[;{}<>\\"'`]|url\s*\(|@import|expression\s*\(/i.test(value), "must be a plain CSS value (no url(), quotes, braces or semicolons)");
|
|
536
|
+
const tokensSchema = z.object({
|
|
537
|
+
accent: color,
|
|
538
|
+
onAccent: color,
|
|
539
|
+
background: color,
|
|
540
|
+
surface: color,
|
|
541
|
+
text: color,
|
|
542
|
+
muted: color,
|
|
543
|
+
border: z.string().regex(/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/, "must be a hex color, alpha allowed"),
|
|
544
|
+
danger: color,
|
|
545
|
+
success: color,
|
|
546
|
+
userBubble: color,
|
|
547
|
+
onUserBubble: color,
|
|
548
|
+
backdrop: cssValue,
|
|
549
|
+
orb: cssValue,
|
|
550
|
+
fontFamily: z.string().max(200).regex(/^[\w\s"',.-]+$/, "must be a font-family list"),
|
|
551
|
+
headingFamily: z.string().max(200).regex(/^[\w\s"',.-]+$/, "must be a font-family list"),
|
|
552
|
+
fontSize: z.string().regex(/^(1[3-9]|20)px$/, "must be 13px to 20px"),
|
|
553
|
+
radius: z.string().regex(/^([0-9]|[12][0-9]|3[0-2])px$/, "must be 0px to 32px")
|
|
554
|
+
}).partial().strict();
|
|
555
|
+
const variantsSchema = z.object({
|
|
556
|
+
avatar: z.enum([
|
|
557
|
+
"orb",
|
|
558
|
+
"icon",
|
|
559
|
+
"initials",
|
|
560
|
+
"none"
|
|
561
|
+
]),
|
|
562
|
+
welcome: z.enum([
|
|
563
|
+
"hero",
|
|
564
|
+
"persona",
|
|
565
|
+
"intro"
|
|
566
|
+
]),
|
|
567
|
+
launcher: z.enum([
|
|
568
|
+
"orb",
|
|
569
|
+
"circle",
|
|
570
|
+
"pill"
|
|
571
|
+
]),
|
|
572
|
+
bubbles: z.enum(["filled", "plain"]),
|
|
573
|
+
surface: z.enum(["solid", "glass"]),
|
|
574
|
+
density: z.enum(["comfortable", "compact"]),
|
|
575
|
+
composer: z.enum(["outline", "gradient"])
|
|
576
|
+
}).partial().strict();
|
|
577
|
+
const themeDefinitionSchema = z.object({
|
|
578
|
+
extends: z.enum(PRESET_NAMES).optional(),
|
|
579
|
+
accent: color.optional(),
|
|
580
|
+
tokens: tokensSchema.optional(),
|
|
581
|
+
dark: tokensSchema.optional(),
|
|
582
|
+
variants: variantsSchema.optional()
|
|
583
|
+
}).strict();
|
|
584
|
+
/** Validates a theme once, so several bots can share it. Throws on invalid values. */
|
|
585
|
+
function defineTheme(definition) {
|
|
586
|
+
const parsed = themeDefinitionSchema.safeParse(definition);
|
|
587
|
+
if (!parsed.success) {
|
|
588
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ");
|
|
589
|
+
throw new TypeError(`[askdare] Invalid theme: ${issues}`);
|
|
590
|
+
}
|
|
591
|
+
return Object.freeze(parsed.data);
|
|
592
|
+
}
|
|
593
|
+
function withAccent(tokens, accent) {
|
|
594
|
+
if (!accent) return tokens;
|
|
595
|
+
const on = readableOn(accent);
|
|
596
|
+
return {
|
|
597
|
+
...tokens,
|
|
598
|
+
accent,
|
|
599
|
+
onAccent: on,
|
|
600
|
+
userBubble: accent,
|
|
601
|
+
onUserBubble: on
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** Preset plus overrides, for both schemes. */
|
|
605
|
+
function resolveTheme(theme, colorScheme) {
|
|
606
|
+
const definition = typeof theme === "string" ? { extends: theme } : theme ?? {};
|
|
607
|
+
const name = definition.extends ?? "aura";
|
|
608
|
+
const preset = PRESETS[name];
|
|
609
|
+
const light = {
|
|
610
|
+
...withAccent(preset.light, definition.accent),
|
|
611
|
+
...definition.tokens
|
|
612
|
+
};
|
|
613
|
+
const dark = {
|
|
614
|
+
...withAccent(preset.dark, definition.dark?.accent ?? definition.accent),
|
|
615
|
+
...definition.tokens,
|
|
616
|
+
...definition.dark
|
|
617
|
+
};
|
|
618
|
+
return Object.freeze({
|
|
619
|
+
preset: name,
|
|
620
|
+
colorScheme,
|
|
621
|
+
light: Object.freeze(light),
|
|
622
|
+
dark: Object.freeze(dark),
|
|
623
|
+
variants: Object.freeze({
|
|
624
|
+
...preset.variants,
|
|
625
|
+
...definition.variants
|
|
626
|
+
})
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/** Readable-contrast problems of a customized theme, for a development warning. */
|
|
630
|
+
function themeWarnings(theme) {
|
|
631
|
+
return (theme.colorScheme === "auto" ? ["light", "dark"] : [theme.colorScheme]).flatMap((scheme) => contrastFailures(theme[scheme]).map((failure) => `${scheme}: ${failure.name} has contrast ${failure.ratio}:1, needs ${failure.required}:1`));
|
|
632
|
+
}
|
|
633
|
+
// src/protocol/version.ts
|
|
634
|
+
/**
|
|
635
|
+
* Version of the SSE wire protocol between the askdare server and its clients.
|
|
636
|
+
* Bumped on any breaking change to event names or payloads (ADR-0006).
|
|
637
|
+
*/
|
|
638
|
+
const PROTOCOL_VERSION = 1;
|
|
639
|
+
/** Response header carrying {@link PROTOCOL_VERSION} on every streaming response. */
|
|
640
|
+
const PROTOCOL_HEADER = "x-askdare-protocol";
|
|
641
|
+
export { themeDefinitionSchema as a, TEXT_KEYS as c, textLocale as d, textsFor as f, resolveTheme as i, TEXT_LOCALES as l, PROTOCOL_VERSION as n, themeWarnings as o, defineTheme as r, PRESET_NAMES as s, PROTOCOL_HEADER as t, canonicalLocale as u };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Attachments
|
|
2
|
+
|
|
3
|
+
Visitors can attach images and PDFs to their messages. The model sees images and reads PDFs (their text and page images).
|
|
4
|
+
|
|
5
|
+
## Turn it on
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
defineBot({
|
|
9
|
+
// …
|
|
10
|
+
attachments: {}, // images and PDFs, up to 4 MB, 3 per message
|
|
11
|
+
});
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Option | Default | Meaning |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `images` | `true` | PNG, JPEG, WebP and GIF |
|
|
17
|
+
| `pdf` | `true` | PDF documents |
|
|
18
|
+
| `maxBytes` | `4_000_000` | Largest file. 4 MB fits Vercel's 4.5 MB request body limit; raise it only on hosts without one |
|
|
19
|
+
| `maxPerMessage` | `3` | Files per message (up to 10) |
|
|
20
|
+
| `maxPdfPages` | `50` | Longest PDF accepted |
|
|
21
|
+
|
|
22
|
+
Uploads are limited per visitor and per IP per hour (`limits.uploadsPerVisitorPerHour`, default 20; `limits.uploadsPerIpPerHour`, default 60) and per bot per day (`limits.uploadsPerDay`, default 500). Files that are uploaded but never sent cost no tokens, but they take up storage in your OpenAI project until they expire.
|
|
23
|
+
|
|
24
|
+
## What the widget does
|
|
25
|
+
|
|
26
|
+
- A paperclip button opens the file picker. Files can also be pasted or dropped onto the chat.
|
|
27
|
+
- Images are downsized to 2048 px on the long edge and re-encoded as JPEG in the browser. That keeps uploads small, drops EXIF data such as the GPS position, and converts HEIC photos where the browser can decode them.
|
|
28
|
+
- Each file is uploaded as soon as it is chosen; the message is sent once every upload has finished.
|
|
29
|
+
|
|
30
|
+
## What the server does
|
|
31
|
+
|
|
32
|
+
- Detects the type from the file's bytes, never from its name or Content-Type. SVG, animated GIFs and anything else are refused.
|
|
33
|
+
- Uploads the file to OpenAI with purpose `user_data`. Files expire after 30 days, and are deleted earlier when the visitor deletes the conversation.
|
|
34
|
+
- Returns an encrypted handle instead of the OpenAI file ID. Handles are bound to the bot and the visitor and expire after 1 hour.
|
|
35
|
+
- Reserves tokens for the files before the turn: about 1,500 per image and 2,000 per PDF page. Pages are counted from the file itself, including compressed object streams; a PDF whose pages can't be counted is charged as if it had `maxPdfPages` pages. When the bot has tools, the reservation covers every model call the turn may make, because each call reads the files again.
|
|
36
|
+
|
|
37
|
+
After a page reload the conversation shows how many files a message had, not the files themselves: OpenAI does not let you download `user_data` files.
|
|
38
|
+
|
|
39
|
+
## Privacy
|
|
40
|
+
|
|
41
|
+
Uploaded files are stored by OpenAI under your project until they expire or are deleted. Mention this in your privacy policy, and keep attachments off for bots that don't need them.
|