Spaces:
Running
on
CPU Upgrade
chores(svelte): migration to svelte 5 (#1685)
Browse files* chores(svelte): migration to svelte 5
* chores(svelte5): remove preventDefault & stopPropagation legacy handlers
* chores(deps): version bumps
* fix: closing modal works properly
* fix: messagte loading reactivity
* fix: loading indicator
* fix: conversation list reactivity
* fix: bug on tool logo rendering
* fix: infinite loading circular dep
* feat: hide new chat button on new chats
* fix: move apply directive to main.css
* fix: remove legacy syntax in `TokensCounter.svelte`
* fix: replace legacy `run` with Svelte 5 `$effect` in NavMenu
* fix: replace legacy `run` with Svelte 5 `$effect` in ScrollToPreviousBtn
* fix: migrate `settings/[modelId]` page to Svelte 5 syntax
* fix: replace legacy `run` with Svelte 5 `$effect` in ToolUpdate component
* fix: migrate ToolEdit component to Svelte 5 syntax
* fix: simplify login required handling in ChatInput
- .prettierrc +0 -1
- package-lock.json +0 -0
- package.json +12 -11
- src/app.html +1 -1
- src/lib/components/AnnouncementBanner.svelte +8 -3
- src/lib/components/AssistantSettings.svelte +43 -35
- src/lib/components/AssistantToolPicker.svelte +22 -8
- src/lib/components/CodeBlock.svelte +7 -3
- src/lib/components/ContinueBtn.svelte +7 -2
- src/lib/components/CopyToClipBoardBtn.svelte +15 -7
- src/lib/components/DisclaimerModal.svelte +3 -1
- src/lib/components/ExpandNavigation.svelte +10 -5
- src/lib/components/HoverTooltip.svelte +9 -4
- src/lib/components/InfiniteScroll.svelte +8 -4
- src/lib/components/LoginModal.svelte +1 -1
- src/lib/components/MobileNav.svelte +30 -17
- src/lib/components/Modal.svelte +17 -12
- src/lib/components/ModelCardMetadata.svelte +8 -5
- src/lib/components/NavConversationItem.svelte +18 -8
- src/lib/components/NavMenu.svelte +29 -22
- src/lib/components/OpenWebSearchResults.svelte +15 -9
- src/lib/components/Pagination.svelte +10 -7
- src/lib/components/PaginationArrow.svelte +7 -3
- src/lib/components/Portal.svelte +8 -3
- src/lib/components/RetryBtn.svelte +7 -2
- src/lib/components/ScrollToBottomBtn.svelte +23 -18
- src/lib/components/ScrollToPreviousBtn.svelte +26 -19
- src/lib/components/StopGeneratingBtn.svelte +7 -2
- src/lib/components/Switch.svelte +7 -3
- src/lib/components/SystemPromptModal.svelte +10 -6
- src/lib/components/Toast.svelte +5 -1
- src/lib/components/TokensCounter.svelte +34 -34
- src/lib/components/ToolBadge.svelte +8 -2
- src/lib/components/ToolLogo.svelte +43 -33
- src/lib/components/ToolsMenu.svelte +26 -13
- src/lib/components/Tooltip.svelte +12 -4
- src/lib/components/UploadBtn.svelte +8 -4
- src/lib/components/WebSearchToggle.svelte +2 -2
- src/lib/components/chat/Alternatives.svelte +10 -6
- src/lib/components/chat/AssistantIntroduction.svelte +29 -24
- src/lib/components/chat/ChatInput.svelte +82 -53
- src/lib/components/chat/ChatIntroduction.svelte +10 -4
- src/lib/components/chat/ChatMessage.svelte +94 -103
- src/lib/components/chat/ChatWindow.svelte +105 -65
- src/lib/components/chat/FileDropzone.svelte +23 -9
- src/lib/components/chat/MarkdownRenderer.svelte +7 -3
- src/lib/components/chat/ModelSwitch.svelte +10 -6
- src/lib/components/chat/OpenReasoningResults.svelte +7 -3
- src/lib/components/chat/ToolUpdate.svelte +57 -49
- src/lib/components/chat/UploadedFile.svelte +36 -20
@@ -3,6 +3,5 @@
|
|
3 |
"trailingComma": "es5",
|
4 |
"printWidth": 100,
|
5 |
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
6 |
-
"pluginSearchDirs": ["."],
|
7 |
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
8 |
}
|
|
|
3 |
"trailingComma": "es5",
|
4 |
"printWidth": 100,
|
5 |
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
|
|
6 |
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
7 |
}
|
The diff for this file is too large to render.
See raw diff
|
|
@@ -9,8 +9,8 @@
|
|
9 |
"preview": "vite preview",
|
10 |
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
11 |
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
12 |
-
"lint": "prettier --
|
13 |
-
"format": "prettier --
|
14 |
"test": "vitest",
|
15 |
"updateLocalEnv": "node --loader ts-node/esm scripts/updateLocalEnv.ts",
|
16 |
"populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts",
|
@@ -21,7 +21,8 @@
|
|
21 |
"@iconify-json/carbon": "^1.1.16",
|
22 |
"@iconify-json/eos-icons": "^1.1.6",
|
23 |
"@sveltejs/adapter-node": "^5.2.0",
|
24 |
-
"@sveltejs/kit": "^2.
|
|
|
25 |
"@tailwindcss/typography": "^0.5.9",
|
26 |
"@types/dompurify": "^3.0.5",
|
27 |
"@types/express": "^4.17.21",
|
@@ -40,22 +41,22 @@
|
|
40 |
"dompurify": "^3.1.6",
|
41 |
"eslint": "^8.28.0",
|
42 |
"eslint-config-prettier": "^8.5.0",
|
43 |
-
"eslint-plugin-svelte": "^2.
|
44 |
"isomorphic-dompurify": "^2.13.0",
|
45 |
"js-yaml": "^4.1.0",
|
46 |
"minimist": "^1.2.8",
|
47 |
"mongodb-memory-server": "^10.1.2",
|
48 |
-
"prettier": "^
|
49 |
-
"prettier-plugin-svelte": "^2.
|
50 |
-
"prettier-plugin-tailwindcss": "^0.
|
51 |
"prom-client": "^15.1.2",
|
52 |
-
"svelte": "^
|
53 |
-
"svelte-check": "^
|
54 |
"ts-node": "^10.9.1",
|
55 |
"tslib": "^2.4.1",
|
56 |
-
"typescript": "^5.
|
57 |
"unplugin-icons": "^0.16.1",
|
58 |
-
"vite": "^
|
59 |
"vite-node": "^1.3.1",
|
60 |
"vitest": "^2.1.9"
|
61 |
},
|
|
|
9 |
"preview": "vite preview",
|
10 |
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
11 |
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
12 |
+
"lint": "prettier --check . && eslint .",
|
13 |
+
"format": "prettier --write .",
|
14 |
"test": "vitest",
|
15 |
"updateLocalEnv": "node --loader ts-node/esm scripts/updateLocalEnv.ts",
|
16 |
"populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts",
|
|
|
21 |
"@iconify-json/carbon": "^1.1.16",
|
22 |
"@iconify-json/eos-icons": "^1.1.6",
|
23 |
"@sveltejs/adapter-node": "^5.2.0",
|
24 |
+
"@sveltejs/kit": "^2.17.1",
|
25 |
+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
26 |
"@tailwindcss/typography": "^0.5.9",
|
27 |
"@types/dompurify": "^3.0.5",
|
28 |
"@types/express": "^4.17.21",
|
|
|
41 |
"dompurify": "^3.1.6",
|
42 |
"eslint": "^8.28.0",
|
43 |
"eslint-config-prettier": "^8.5.0",
|
44 |
+
"eslint-plugin-svelte": "^2.45.1",
|
45 |
"isomorphic-dompurify": "^2.13.0",
|
46 |
"js-yaml": "^4.1.0",
|
47 |
"minimist": "^1.2.8",
|
48 |
"mongodb-memory-server": "^10.1.2",
|
49 |
+
"prettier": "^3.1.0",
|
50 |
+
"prettier-plugin-svelte": "^3.2.6",
|
51 |
+
"prettier-plugin-tailwindcss": "^0.6.11",
|
52 |
"prom-client": "^15.1.2",
|
53 |
+
"svelte": "^5.0.0",
|
54 |
+
"svelte-check": "^4.0.0",
|
55 |
"ts-node": "^10.9.1",
|
56 |
"tslib": "^2.4.1",
|
57 |
+
"typescript": "^5.5.0",
|
58 |
"unplugin-icons": "^0.16.1",
|
59 |
+
"vite": "^6.1.0",
|
60 |
"vite-node": "^1.3.1",
|
61 |
"vitest": "^2.1.9"
|
62 |
},
|
@@ -1,4 +1,4 @@
|
|
1 |
-
<!
|
2 |
<html lang="en" class="h-full">
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
|
|
1 |
+
<!doctype html>
|
2 |
<html lang="en" class="h-full">
|
3 |
<head>
|
4 |
<meta charset="utf-8" />
|
@@ -1,6 +1,11 @@
|
|
1 |
<script lang="ts">
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
4 |
</script>
|
5 |
|
6 |
<div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
|
@@ -10,6 +15,6 @@
|
|
10 |
>
|
11 |
{title}
|
12 |
<div class="ml-auto shrink-0">
|
13 |
-
|
14 |
</div>
|
15 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
+
interface Props {
|
3 |
+
title?: string;
|
4 |
+
classNames?: string;
|
5 |
+
children?: import("svelte").Snippet;
|
6 |
+
}
|
7 |
+
|
8 |
+
let { title = "", classNames = "", children }: Props = $props();
|
9 |
</script>
|
10 |
|
11 |
<div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
|
|
|
15 |
>
|
16 |
{title}
|
17 |
<div class="ml-auto shrink-0">
|
18 |
+
{@render children?.()}
|
19 |
</div>
|
20 |
</div>
|
@@ -31,18 +31,22 @@
|
|
31 |
|
32 |
type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
37 |
|
38 |
-
let files: FileList | null = null;
|
39 |
const settings = useSettingsStore();
|
40 |
-
let modelId = "";
|
41 |
-
let systemPrompt = assistant?.preprompt ?? "";
|
42 |
-
let dynamicPrompt = assistant?.dynamicPrompt ?? false;
|
43 |
-
let showModelSettings = Object.values(assistant?.generateSettings ?? {}).some((v) => !!v);
|
44 |
|
45 |
-
let compress: typeof readAndCompressImage | null = null;
|
46 |
|
47 |
onMount(async () => {
|
48 |
const module = await import("browser-image-resizer");
|
@@ -51,10 +55,10 @@
|
|
51 |
modelId = findCurrentModel(models, assistant ? assistant.modelId : $settings.activeModel).id;
|
52 |
});
|
53 |
|
54 |
-
let inputMessage1 = assistant?.exampleInputs[0] ?? "";
|
55 |
-
let inputMessage2 = assistant?.exampleInputs[1] ?? "";
|
56 |
-
let inputMessage3 = assistant?.exampleInputs[2] ?? "";
|
57 |
-
let inputMessage4 = assistant?.exampleInputs[3] ?? "";
|
58 |
|
59 |
function resetErrors() {
|
60 |
if (form) {
|
@@ -83,23 +87,25 @@
|
|
83 |
return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
|
84 |
}
|
85 |
|
86 |
-
let deleteExistingAvatar = false;
|
87 |
|
88 |
-
let loading = false;
|
89 |
|
90 |
-
let ragMode: false | "links" | "domains" | "all" =
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
97 |
|
98 |
-
let tools = assistant?.tools ?? [];
|
99 |
const regex = /{{\s?(get|post|url|today)(=.*?)?\s?}}/g;
|
100 |
|
101 |
-
|
102 |
-
|
103 |
</script>
|
104 |
|
105 |
<form
|
@@ -184,7 +190,7 @@
|
|
184 |
name="avatar"
|
185 |
id="avatar"
|
186 |
class="hidden"
|
187 |
-
|
188 |
/>
|
189 |
|
190 |
{#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)}
|
@@ -213,7 +219,9 @@
|
|
213 |
<div class="mx-auto w-max pt-1">
|
214 |
<button
|
215 |
type="button"
|
216 |
-
|
|
|
|
|
217 |
files = null;
|
218 |
deleteExistingAvatar = true;
|
219 |
}}
|
@@ -253,7 +261,7 @@
|
|
253 |
class="h-15 w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
|
254 |
placeholder="It knows everything about python"
|
255 |
value={assistant?.description ?? ""}
|
256 |
-
|
257 |
<p class="text-xs text-red-500">{getError("description", form)}</p>
|
258 |
</label>
|
259 |
|
@@ -268,14 +276,14 @@
|
|
268 |
{#each models.filter((model) => !model.unlisted) as model}
|
269 |
<option value={model.id}>{model.displayName}</option>
|
270 |
{/each}
|
271 |
-
<p class="text-xs text-red-500">{getError("modelId", form)}</p>
|
272 |
</select>
|
|
|
273 |
<button
|
274 |
type="button"
|
275 |
class="flex aspect-square items-center gap-2 whitespace-nowrap rounded-lg border px-3 {showModelSettings
|
276 |
? 'border-blue-500/20 bg-blue-50 text-blue-600'
|
277 |
: ''}"
|
278 |
-
|
279 |
><CarbonSettingsAdjust class="text-xs" /></button
|
280 |
>
|
281 |
</div>
|
@@ -443,7 +451,7 @@
|
|
443 |
<label class="mt-1">
|
444 |
<input
|
445 |
checked={!ragMode}
|
446 |
-
|
447 |
type="radio"
|
448 |
name="ragMode"
|
449 |
value={false}
|
@@ -460,7 +468,7 @@
|
|
460 |
<label class="mt-1">
|
461 |
<input
|
462 |
checked={ragMode === "all"}
|
463 |
-
|
464 |
type="radio"
|
465 |
name="ragMode"
|
466 |
value={"all"}
|
@@ -476,7 +484,7 @@
|
|
476 |
<label class="mt-1">
|
477 |
<input
|
478 |
checked={ragMode === "domains"}
|
479 |
-
|
480 |
type="radio"
|
481 |
name="ragMode"
|
482 |
value={false}
|
@@ -502,7 +510,7 @@
|
|
502 |
<label class="mt-1">
|
503 |
<input
|
504 |
checked={ragMode === "links"}
|
505 |
-
|
506 |
type="radio"
|
507 |
name="ragMode"
|
508 |
value={false}
|
@@ -576,7 +584,7 @@
|
|
576 |
class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
|
577 |
placeholder="You'll act as..."
|
578 |
bind:value={systemPrompt}
|
579 |
-
|
580 |
{#if modelId}
|
581 |
{@const model = models.find((_model) => _model.id === modelId)}
|
582 |
{#if model?.tokenizer && systemPrompt}
|
|
|
31 |
|
32 |
type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
|
33 |
|
34 |
+
interface Props {
|
35 |
+
form: ActionData;
|
36 |
+
assistant?: AssistantFront | undefined;
|
37 |
+
models?: Model[];
|
38 |
+
}
|
39 |
+
|
40 |
+
let { form = $bindable(), assistant = undefined, models = [] }: Props = $props();
|
41 |
|
42 |
+
let files: FileList | null = $state(null);
|
43 |
const settings = useSettingsStore();
|
44 |
+
let modelId = $state("");
|
45 |
+
let systemPrompt = $state(assistant?.preprompt ?? "");
|
46 |
+
let dynamicPrompt = $state(assistant?.dynamicPrompt ?? false);
|
47 |
+
let showModelSettings = $state(Object.values(assistant?.generateSettings ?? {}).some((v) => !!v));
|
48 |
|
49 |
+
let compress: typeof readAndCompressImage | null = $state(null);
|
50 |
|
51 |
onMount(async () => {
|
52 |
const module = await import("browser-image-resizer");
|
|
|
55 |
modelId = findCurrentModel(models, assistant ? assistant.modelId : $settings.activeModel).id;
|
56 |
});
|
57 |
|
58 |
+
let inputMessage1 = $state(assistant?.exampleInputs[0] ?? "");
|
59 |
+
let inputMessage2 = $state(assistant?.exampleInputs[1] ?? "");
|
60 |
+
let inputMessage3 = $state(assistant?.exampleInputs[2] ?? "");
|
61 |
+
let inputMessage4 = $state(assistant?.exampleInputs[3] ?? "");
|
62 |
|
63 |
function resetErrors() {
|
64 |
if (form) {
|
|
|
87 |
return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
|
88 |
}
|
89 |
|
90 |
+
let deleteExistingAvatar = $state(false);
|
91 |
|
92 |
+
let loading = $state(false);
|
93 |
|
94 |
+
let ragMode: false | "links" | "domains" | "all" = $state(
|
95 |
+
assistant?.rag?.allowAllDomains
|
96 |
+
? "all"
|
97 |
+
: (assistant?.rag?.allowedLinks?.length ?? 0 > 0)
|
98 |
+
? "links"
|
99 |
+
: (assistant?.rag?.allowedDomains?.length ?? 0) > 0
|
100 |
+
? "domains"
|
101 |
+
: false
|
102 |
+
);
|
103 |
|
104 |
+
let tools = $state(assistant?.tools ?? []);
|
105 |
const regex = /{{\s?(get|post|url|today)(=.*?)?\s?}}/g;
|
106 |
|
107 |
+
let templateVariables = $derived([...systemPrompt.matchAll(regex)]);
|
108 |
+
let selectedModel = $derived(models.find((m) => m.id === modelId));
|
109 |
</script>
|
110 |
|
111 |
<form
|
|
|
190 |
name="avatar"
|
191 |
id="avatar"
|
192 |
class="hidden"
|
193 |
+
onchange={onFilesChange}
|
194 |
/>
|
195 |
|
196 |
{#if (files && files[0]) || (assistant?.avatar && !deleteExistingAvatar)}
|
|
|
219 |
<div class="mx-auto w-max pt-1">
|
220 |
<button
|
221 |
type="button"
|
222 |
+
onclick={(e) => {
|
223 |
+
e.preventDefault();
|
224 |
+
e.stopPropagation();
|
225 |
files = null;
|
226 |
deleteExistingAvatar = true;
|
227 |
}}
|
|
|
261 |
class="h-15 w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
|
262 |
placeholder="It knows everything about python"
|
263 |
value={assistant?.description ?? ""}
|
264 |
+
></textarea>
|
265 |
<p class="text-xs text-red-500">{getError("description", form)}</p>
|
266 |
</label>
|
267 |
|
|
|
276 |
{#each models.filter((model) => !model.unlisted) as model}
|
277 |
<option value={model.id}>{model.displayName}</option>
|
278 |
{/each}
|
|
|
279 |
</select>
|
280 |
+
<p class="text-xs text-red-500">{getError("modelId", form)}</p>
|
281 |
<button
|
282 |
type="button"
|
283 |
class="flex aspect-square items-center gap-2 whitespace-nowrap rounded-lg border px-3 {showModelSettings
|
284 |
? 'border-blue-500/20 bg-blue-50 text-blue-600'
|
285 |
: ''}"
|
286 |
+
onclick={() => (showModelSettings = !showModelSettings)}
|
287 |
><CarbonSettingsAdjust class="text-xs" /></button
|
288 |
>
|
289 |
</div>
|
|
|
451 |
<label class="mt-1">
|
452 |
<input
|
453 |
checked={!ragMode}
|
454 |
+
onchange={() => (ragMode = false)}
|
455 |
type="radio"
|
456 |
name="ragMode"
|
457 |
value={false}
|
|
|
468 |
<label class="mt-1">
|
469 |
<input
|
470 |
checked={ragMode === "all"}
|
471 |
+
onchange={() => (ragMode = "all")}
|
472 |
type="radio"
|
473 |
name="ragMode"
|
474 |
value={"all"}
|
|
|
484 |
<label class="mt-1">
|
485 |
<input
|
486 |
checked={ragMode === "domains"}
|
487 |
+
onchange={() => (ragMode = "domains")}
|
488 |
type="radio"
|
489 |
name="ragMode"
|
490 |
value={false}
|
|
|
510 |
<label class="mt-1">
|
511 |
<input
|
512 |
checked={ragMode === "links"}
|
513 |
+
onchange={() => (ragMode = "links")}
|
514 |
type="radio"
|
515 |
name="ragMode"
|
516 |
value={false}
|
|
|
584 |
class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
|
585 |
placeholder="You'll act as..."
|
586 |
bind:value={systemPrompt}
|
587 |
+
></textarea>
|
588 |
{#if modelId}
|
589 |
{@const model = models.find((_model) => _model.id === modelId)}
|
590 |
{#if model?.tokenizer && systemPrompt}
|
@@ -15,9 +15,13 @@
|
|
15 |
icon: ToolLogoIcon;
|
16 |
}
|
17 |
|
18 |
-
|
|
|
|
|
|
|
|
|
19 |
|
20 |
-
let selectedValues: ToolSuggestion[] = [];
|
21 |
|
22 |
onMount(async () => {
|
23 |
selectedValues = await Promise.all(
|
@@ -27,10 +31,10 @@
|
|
27 |
await fetchSuggestions("");
|
28 |
});
|
29 |
|
30 |
-
let inputValue = "";
|
31 |
let maxValues = 3;
|
32 |
|
33 |
-
let suggestions: ToolSuggestion[] = [];
|
34 |
|
35 |
async function fetchSuggestions(query: string) {
|
36 |
suggestions = (await fetch(`${base}/api/tools/search?q=${query}`).then((res) =>
|
@@ -61,7 +65,9 @@
|
|
61 |
<div
|
62 |
class="flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
|
63 |
>
|
64 |
-
|
|
|
|
|
65 |
<div class="flex flex-col items-center justify-center py-1">
|
66 |
<a
|
67 |
href={`${base}/tools/${value._id}`}
|
@@ -81,7 +87,11 @@
|
|
81 |
{/if}
|
82 |
</div>
|
83 |
<button
|
84 |
-
|
|
|
|
|
|
|
|
|
85 |
class="text-lg text-gray-600"
|
86 |
>
|
87 |
<CarbonClose />
|
@@ -96,7 +106,7 @@
|
|
96 |
<input
|
97 |
type="text"
|
98 |
bind:value={inputValue}
|
99 |
-
|
100 |
inputValue = ev.currentTarget.value;
|
101 |
debouncedFetch(inputValue);
|
102 |
}}
|
@@ -117,7 +127,11 @@
|
|
117 |
{:else}
|
118 |
{#each suggestions as suggestion}
|
119 |
<button
|
120 |
-
|
|
|
|
|
|
|
|
|
121 |
class="w-full cursor-pointer px-3 py-2 text-left hover:bg-blue-500 hover:text-white"
|
122 |
>
|
123 |
{suggestion.displayName}
|
|
|
15 |
icon: ToolLogoIcon;
|
16 |
}
|
17 |
|
18 |
+
interface Props {
|
19 |
+
toolIds?: string[];
|
20 |
+
}
|
21 |
+
|
22 |
+
let { toolIds = $bindable([]) }: Props = $props();
|
23 |
|
24 |
+
let selectedValues: ToolSuggestion[] = $state([]);
|
25 |
|
26 |
onMount(async () => {
|
27 |
selectedValues = await Promise.all(
|
|
|
31 |
await fetchSuggestions("");
|
32 |
});
|
33 |
|
34 |
+
let inputValue = $state("");
|
35 |
let maxValues = 3;
|
36 |
|
37 |
+
let suggestions: ToolSuggestion[] = $state([]);
|
38 |
|
39 |
async function fetchSuggestions(query: string) {
|
40 |
suggestions = (await fetch(`${base}/api/tools/search?q=${query}`).then((res) =>
|
|
|
65 |
<div
|
66 |
class="flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
|
67 |
>
|
68 |
+
{#key value.color + value.icon}
|
69 |
+
<ToolLogo color={value.color} icon={value.icon} size="sm" />
|
70 |
+
{/key}
|
71 |
<div class="flex flex-col items-center justify-center py-1">
|
72 |
<a
|
73 |
href={`${base}/tools/${value._id}`}
|
|
|
87 |
{/if}
|
88 |
</div>
|
89 |
<button
|
90 |
+
onclick={(e) => {
|
91 |
+
e.preventDefault();
|
92 |
+
e.stopPropagation();
|
93 |
+
removeValue(value._id);
|
94 |
+
}}
|
95 |
class="text-lg text-gray-600"
|
96 |
>
|
97 |
<CarbonClose />
|
|
|
106 |
<input
|
107 |
type="text"
|
108 |
bind:value={inputValue}
|
109 |
+
oninput={(ev) => {
|
110 |
inputValue = ev.currentTarget.value;
|
111 |
debouncedFetch(inputValue);
|
112 |
}}
|
|
|
127 |
{:else}
|
128 |
{#each suggestions as suggestion}
|
129 |
<button
|
130 |
+
onclick={(e) => {
|
131 |
+
e.preventDefault();
|
132 |
+
e.stopPropagation();
|
133 |
+
addValue(suggestion);
|
134 |
+
}}
|
135 |
class="w-full cursor-pointer px-3 py-2 text-left hover:bg-blue-500 hover:text-white"
|
136 |
>
|
137 |
{suggestion.displayName}
|
@@ -3,10 +3,14 @@
|
|
3 |
import DOMPurify from "isomorphic-dompurify";
|
4 |
import hljs from "highlight.js";
|
5 |
|
6 |
-
|
7 |
-
|
|
|
|
|
8 |
|
9 |
-
|
|
|
|
|
10 |
</script>
|
11 |
|
12 |
<div class="group relative my-4 rounded-lg">
|
|
|
3 |
import DOMPurify from "isomorphic-dompurify";
|
4 |
import hljs from "highlight.js";
|
5 |
|
6 |
+
interface Props {
|
7 |
+
code?: string;
|
8 |
+
lang?: string;
|
9 |
+
}
|
10 |
|
11 |
+
let { code = "", lang = "" }: Props = $props();
|
12 |
+
|
13 |
+
let highlightedCode = $derived(hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value);
|
14 |
</script>
|
15 |
|
16 |
<div class="group relative my-4 rounded-lg">
|
@@ -1,12 +1,17 @@
|
|
1 |
<script lang="ts">
|
2 |
import CarbonContinue from "~icons/carbon/continue";
|
3 |
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
5 |
</script>
|
6 |
|
7 |
<button
|
8 |
type="button"
|
9 |
-
|
10 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
|
11 |
>
|
12 |
<CarbonContinue class="mr-2 text-xs " /> Continue
|
|
|
1 |
<script lang="ts">
|
2 |
import CarbonContinue from "~icons/carbon/continue";
|
3 |
|
4 |
+
interface Props {
|
5 |
+
classNames?: string;
|
6 |
+
onClick?: () => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
let { classNames = "", onClick }: Props = $props();
|
10 |
</script>
|
11 |
|
12 |
<button
|
13 |
type="button"
|
14 |
+
onclick={onClick}
|
15 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
|
16 |
>
|
17 |
<CarbonContinue class="mr-2 text-xs " /> Continue
|
@@ -4,10 +4,16 @@
|
|
4 |
import IconCopy from "./icons/IconCopy.svelte";
|
5 |
import Tooltip from "./Tooltip.svelte";
|
6 |
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
let
|
|
|
|
|
11 |
let timeout: ReturnType<typeof setTimeout>;
|
12 |
|
13 |
const unsecuredCopy = (text: string) => {
|
@@ -58,13 +64,15 @@
|
|
58 |
class={classNames}
|
59 |
title={"Copy to clipboard"}
|
60 |
type="button"
|
61 |
-
|
62 |
-
|
|
|
|
|
63 |
>
|
64 |
<div class="relative">
|
65 |
-
|
66 |
<IconCopy classNames="h-[1.14em] w-[1.14em]" />
|
67 |
-
|
68 |
|
69 |
<Tooltip classNames={isSuccess ? "opacity-100" : "opacity-0"} />
|
70 |
</div>
|
|
|
4 |
import IconCopy from "./icons/IconCopy.svelte";
|
5 |
import Tooltip from "./Tooltip.svelte";
|
6 |
|
7 |
+
interface Props {
|
8 |
+
classNames?: string;
|
9 |
+
value: string;
|
10 |
+
children?: import("svelte").Snippet;
|
11 |
+
onClick?: () => void;
|
12 |
+
}
|
13 |
|
14 |
+
let { classNames = "", value, children, onClick }: Props = $props();
|
15 |
+
|
16 |
+
let isSuccess = $state(false);
|
17 |
let timeout: ReturnType<typeof setTimeout>;
|
18 |
|
19 |
const unsecuredCopy = (text: string) => {
|
|
|
64 |
class={classNames}
|
65 |
title={"Copy to clipboard"}
|
66 |
type="button"
|
67 |
+
onclick={() => {
|
68 |
+
onClick?.();
|
69 |
+
handleClick();
|
70 |
+
}}
|
71 |
>
|
72 |
<div class="relative">
|
73 |
+
{#if children}{@render children()}{:else}
|
74 |
<IconCopy classNames="h-[1.14em] w-[1.14em]" />
|
75 |
+
{/if}
|
76 |
|
77 |
<Tooltip classNames={isSuccess ? "opacity-100" : "opacity-0"} />
|
78 |
</div>
|
@@ -34,7 +34,9 @@
|
|
34 |
class:bg-white={$page.data.loginEnabled}
|
35 |
class:text-gray-800={$page.data.loginEnabled}
|
36 |
class:hover:bg-slate-100={$page.data.loginEnabled}
|
37 |
-
|
|
|
|
|
38 |
if (!cookiesAreEnabled()) {
|
39 |
window.open(window.location.href, "_blank");
|
40 |
}
|
|
|
34 |
class:bg-white={$page.data.loginEnabled}
|
35 |
class:text-gray-800={$page.data.loginEnabled}
|
36 |
class:hover:bg-slate-100={$page.data.loginEnabled}
|
37 |
+
onclick={(e) => {
|
38 |
+
e.preventDefault();
|
39 |
+
e.stopPropagation();
|
40 |
if (!cookiesAreEnabled()) {
|
41 |
window.open(window.location.href, "_blank");
|
42 |
}
|
@@ -1,16 +1,21 @@
|
|
1 |
<script lang="ts">
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
4 |
</script>
|
5 |
|
6 |
<button
|
7 |
-
|
8 |
class="{classNames} group flex h-16 w-6 flex-col items-center justify-center -space-y-1 outline-none *:h-3 *:w-1 *:rounded-full *:hover:bg-gray-300 dark:*:hover:bg-gray-600 max-md:hidden {!isCollapsed
|
9 |
? '*:bg-gray-200/70 dark:*:bg-gray-800'
|
10 |
: '*:bg-gray-200 dark:*:bg-gray-700'}"
|
11 |
name="sidebar-toggle"
|
12 |
aria-label="Toggle sidebar navigation"
|
13 |
>
|
14 |
-
<div class={!isCollapsed ? "group-hover:rotate-[20deg]" : "group-hover:-rotate-[20deg]"}
|
15 |
-
<div class={!isCollapsed ? "group-hover:-rotate-[20deg]" : "group-hover:rotate-[20deg]"}
|
16 |
</button>
|
|
|
1 |
<script lang="ts">
|
2 |
+
interface Props {
|
3 |
+
isCollapsed: boolean;
|
4 |
+
onClick: () => void;
|
5 |
+
classNames: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
let { isCollapsed, classNames, onClick }: Props = $props();
|
9 |
</script>
|
10 |
|
11 |
<button
|
12 |
+
onclick={onClick}
|
13 |
class="{classNames} group flex h-16 w-6 flex-col items-center justify-center -space-y-1 outline-none *:h-3 *:w-1 *:rounded-full *:hover:bg-gray-300 dark:*:hover:bg-gray-600 max-md:hidden {!isCollapsed
|
14 |
? '*:bg-gray-200/70 dark:*:bg-gray-800'
|
15 |
: '*:bg-gray-200 dark:*:bg-gray-700'}"
|
16 |
name="sidebar-toggle"
|
17 |
aria-label="Toggle sidebar navigation"
|
18 |
>
|
19 |
+
<div class={!isCollapsed ? "group-hover:rotate-[20deg]" : "group-hover:-rotate-[20deg]"}></div>
|
20 |
+
<div class={!isCollapsed ? "group-hover:-rotate-[20deg]" : "group-hover:rotate-[20deg]"}></div>
|
21 |
</button>
|
@@ -1,7 +1,12 @@
|
|
1 |
<script lang="ts">
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
const positionClasses = {
|
7 |
top: "bottom-full mb-2",
|
@@ -12,7 +17,7 @@
|
|
12 |
</script>
|
13 |
|
14 |
<div class="group/tooltip inline-block md:relative">
|
15 |
-
|
16 |
|
17 |
<div
|
18 |
class="
|
|
|
1 |
<script lang="ts">
|
2 |
+
interface Props {
|
3 |
+
label?: string;
|
4 |
+
position?: "top" | "bottom" | "left" | "right";
|
5 |
+
TooltipClassNames?: string;
|
6 |
+
children?: import("svelte").Snippet;
|
7 |
+
}
|
8 |
+
|
9 |
+
let { label = "", position = "bottom", TooltipClassNames = "", children }: Props = $props();
|
10 |
|
11 |
const positionClasses = {
|
12 |
top: "bottom-full mb-2",
|
|
|
17 |
</script>
|
18 |
|
19 |
<div class="group/tooltip inline-block md:relative">
|
20 |
+
{@render children?.()}
|
21 |
|
22 |
<div
|
23 |
class="
|
@@ -2,11 +2,15 @@
|
|
2 |
import { onMount, createEventDispatcher } from "svelte";
|
3 |
|
4 |
const dispatch = createEventDispatcher();
|
5 |
-
let loader: HTMLDivElement;
|
6 |
let observer: IntersectionObserver;
|
7 |
let intervalId: ReturnType<typeof setInterval> | undefined;
|
8 |
|
9 |
onMount(() => {
|
|
|
|
|
|
|
|
|
10 |
observer = new IntersectionObserver((entries) => {
|
11 |
entries.forEach((entry) => {
|
12 |
if (entry.isIntersecting) {
|
@@ -40,7 +44,7 @@
|
|
40 |
</script>
|
41 |
|
42 |
<div bind:this={loader} class="flex animate-pulse flex-col gap-4">
|
43 |
-
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"
|
44 |
-
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"
|
45 |
-
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"
|
46 |
</div>
|
|
|
2 |
import { onMount, createEventDispatcher } from "svelte";
|
3 |
|
4 |
const dispatch = createEventDispatcher();
|
5 |
+
let loader: HTMLDivElement | undefined = $state();
|
6 |
let observer: IntersectionObserver;
|
7 |
let intervalId: ReturnType<typeof setInterval> | undefined;
|
8 |
|
9 |
onMount(() => {
|
10 |
+
if (!loader) {
|
11 |
+
return;
|
12 |
+
}
|
13 |
+
|
14 |
observer = new IntersectionObserver((entries) => {
|
15 |
entries.forEach((entry) => {
|
16 |
if (entry.isIntersecting) {
|
|
|
44 |
</script>
|
45 |
|
46 |
<div bind:this={loader} class="flex animate-pulse flex-col gap-4">
|
47 |
+
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"></div>
|
48 |
+
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"></div>
|
49 |
+
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"></div>
|
50 |
</div>
|
@@ -47,7 +47,7 @@
|
|
47 |
{:else}
|
48 |
<button
|
49 |
class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
50 |
-
|
51 |
if (!cookiesAreEnabled()) {
|
52 |
e.preventDefault();
|
53 |
window.open(window.location.href, "_blank");
|
|
|
47 |
{:else}
|
48 |
<button
|
49 |
class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
50 |
+
onclick={(e) => {
|
51 |
if (!cookiesAreEnabled()) {
|
52 |
e.preventDefault();
|
53 |
window.open(window.location.href, "_blank");
|
@@ -1,4 +1,6 @@
|
|
1 |
<script lang="ts">
|
|
|
|
|
2 |
import { navigating } from "$app/stores";
|
3 |
import { createEventDispatcher } from "svelte";
|
4 |
import { browser } from "$app/environment";
|
@@ -9,25 +11,36 @@
|
|
9 |
import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
|
10 |
import IconNew from "$lib/components/icons/IconNew.svelte";
|
11 |
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
|
|
|
|
|
16 |
|
17 |
-
let closeEl: HTMLButtonElement;
|
18 |
-
let openEl: HTMLButtonElement;
|
19 |
|
20 |
const dispatch = createEventDispatcher();
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
|
|
|
|
25 |
|
26 |
-
|
27 |
-
closeEl
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
31 |
</script>
|
32 |
|
33 |
<nav
|
@@ -36,12 +49,12 @@
|
|
36 |
<button
|
37 |
type="button"
|
38 |
class="-ml-3 flex size-12 shrink-0 items-center justify-center text-lg"
|
39 |
-
|
40 |
aria-label="Open menu"
|
41 |
bind:this={openEl}><CarbonTextAlignJustify /></button
|
42 |
>
|
43 |
{#await title}
|
44 |
-
<div class="flex h-full items-center justify-center"
|
45 |
{:then title}
|
46 |
<span class="truncate px-4">{title ?? ""}</span>
|
47 |
{/await}
|
@@ -60,10 +73,10 @@
|
|
60 |
<button
|
61 |
type="button"
|
62 |
class="-mr-3 ml-auto flex size-12 items-center justify-center text-lg"
|
63 |
-
|
64 |
aria-label="Close menu"
|
65 |
bind:this={closeEl}><CarbonClose /></button
|
66 |
>
|
67 |
</div>
|
68 |
-
|
69 |
</nav>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { run } from "svelte/legacy";
|
3 |
+
|
4 |
import { navigating } from "$app/stores";
|
5 |
import { createEventDispatcher } from "svelte";
|
6 |
import { browser } from "$app/environment";
|
|
|
11 |
import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
|
12 |
import IconNew from "$lib/components/icons/IconNew.svelte";
|
13 |
|
14 |
+
interface Props {
|
15 |
+
isOpen?: boolean;
|
16 |
+
title: string | undefined;
|
17 |
+
children?: import("svelte").Snippet;
|
18 |
+
}
|
19 |
+
|
20 |
+
let { isOpen = false, title = $bindable(), children }: Props = $props();
|
21 |
|
22 |
+
run(() => {
|
23 |
+
title = title ?? "New Chat";
|
24 |
+
});
|
25 |
|
26 |
+
let closeEl: HTMLButtonElement | undefined = $state();
|
27 |
+
let openEl: HTMLButtonElement | undefined = $state();
|
28 |
|
29 |
const dispatch = createEventDispatcher();
|
30 |
|
31 |
+
run(() => {
|
32 |
+
if ($navigating) {
|
33 |
+
dispatch("toggle", false);
|
34 |
+
}
|
35 |
+
});
|
36 |
|
37 |
+
run(() => {
|
38 |
+
if (isOpen && closeEl) {
|
39 |
+
closeEl.focus();
|
40 |
+
} else if (!isOpen && browser && document.activeElement === closeEl) {
|
41 |
+
openEl?.focus();
|
42 |
+
}
|
43 |
+
});
|
44 |
</script>
|
45 |
|
46 |
<nav
|
|
|
49 |
<button
|
50 |
type="button"
|
51 |
class="-ml-3 flex size-12 shrink-0 items-center justify-center text-lg"
|
52 |
+
onclick={() => dispatch("toggle", true)}
|
53 |
aria-label="Open menu"
|
54 |
bind:this={openEl}><CarbonTextAlignJustify /></button
|
55 |
>
|
56 |
{#await title}
|
57 |
+
<div class="flex h-full items-center justify-center"></div>
|
58 |
{:then title}
|
59 |
<span class="truncate px-4">{title ?? ""}</span>
|
60 |
{/await}
|
|
|
73 |
<button
|
74 |
type="button"
|
75 |
class="-mr-3 ml-auto flex size-12 items-center justify-center text-lg"
|
76 |
+
onclick={() => dispatch("toggle", false)}
|
77 |
aria-label="Close menu"
|
78 |
bind:this={closeEl}><CarbonClose /></button
|
79 |
>
|
80 |
</div>
|
81 |
+
{@render children?.()}
|
82 |
</nav>
|
@@ -5,10 +5,15 @@
|
|
5 |
import Portal from "./Portal.svelte";
|
6 |
import { browser } from "$app/environment";
|
7 |
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
let backdropEl: HTMLDivElement;
|
11 |
-
let modalEl: HTMLDivElement;
|
12 |
|
13 |
const dispatch = createEventDispatcher<{ close: void }>();
|
14 |
|
@@ -31,25 +36,25 @@
|
|
31 |
|
32 |
onMount(() => {
|
33 |
document.getElementById("app")?.setAttribute("inert", "true");
|
34 |
-
modalEl
|
35 |
});
|
36 |
|
37 |
onDestroy(() => {
|
38 |
if (!browser) return;
|
39 |
-
|
40 |
-
if (document.querySelectorAll('[role="dialog"]:not(#app *)').length === 1) {
|
41 |
-
document.getElementById("app")?.removeAttribute("inert");
|
42 |
-
}
|
43 |
});
|
44 |
</script>
|
45 |
|
46 |
<Portal>
|
47 |
-
<!-- svelte-ignore
|
48 |
<div
|
49 |
role="presentation"
|
50 |
tabindex="-1"
|
51 |
bind:this={backdropEl}
|
52 |
-
|
|
|
|
|
|
|
53 |
transition:fade|local={{ easing: cubicOut, duration: 300 }}
|
54 |
class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
|
55 |
>
|
@@ -57,10 +62,10 @@
|
|
57 |
role="dialog"
|
58 |
tabindex="-1"
|
59 |
bind:this={modalEl}
|
60 |
-
|
61 |
class="max-h-[90dvh] overflow-y-auto overflow-x-hidden rounded-2xl bg-white shadow-2xl outline-none sm:-mt-10 {width}"
|
62 |
>
|
63 |
-
|
64 |
</div>
|
65 |
</div>
|
66 |
</Portal>
|
|
|
5 |
import Portal from "./Portal.svelte";
|
6 |
import { browser } from "$app/environment";
|
7 |
|
8 |
+
interface Props {
|
9 |
+
width?: string;
|
10 |
+
children?: import("svelte").Snippet;
|
11 |
+
}
|
12 |
+
|
13 |
+
let { width = "max-w-sm", children }: Props = $props();
|
14 |
|
15 |
+
let backdropEl: HTMLDivElement | undefined = $state();
|
16 |
+
let modalEl: HTMLDivElement | undefined = $state();
|
17 |
|
18 |
const dispatch = createEventDispatcher<{ close: void }>();
|
19 |
|
|
|
36 |
|
37 |
onMount(() => {
|
38 |
document.getElementById("app")?.setAttribute("inert", "true");
|
39 |
+
modalEl?.focus();
|
40 |
});
|
41 |
|
42 |
onDestroy(() => {
|
43 |
if (!browser) return;
|
44 |
+
document.getElementById("app")?.removeAttribute("inert");
|
|
|
|
|
|
|
45 |
});
|
46 |
</script>
|
47 |
|
48 |
<Portal>
|
49 |
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
50 |
<div
|
51 |
role="presentation"
|
52 |
tabindex="-1"
|
53 |
bind:this={backdropEl}
|
54 |
+
onclick={(e) => {
|
55 |
+
e.stopPropagation();
|
56 |
+
handleBackdropClick(e);
|
57 |
+
}}
|
58 |
transition:fade|local={{ easing: cubicOut, duration: 300 }}
|
59 |
class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
|
60 |
>
|
|
|
62 |
role="dialog"
|
63 |
tabindex="-1"
|
64 |
bind:this={modalEl}
|
65 |
+
onkeydown={handleKeydown}
|
66 |
class="max-h-[90dvh] overflow-y-auto overflow-x-hidden rounded-2xl bg-white shadow-2xl outline-none sm:-mt-10 {width}"
|
67 |
>
|
68 |
+
{@render children?.()}
|
69 |
</div>
|
70 |
</div>
|
71 |
</Portal>
|
@@ -5,12 +5,15 @@
|
|
5 |
import CarbonCode from "~icons/carbon/code";
|
6 |
import type { Model } from "$lib/types/Model";
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
12 |
|
13 |
-
|
14 |
</script>
|
15 |
|
16 |
<div
|
|
|
5 |
import CarbonCode from "~icons/carbon/code";
|
6 |
import type { Model } from "$lib/types/Model";
|
7 |
|
8 |
+
interface Props {
|
9 |
+
model: Pick<
|
10 |
+
Model,
|
11 |
+
"name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl" | "hasInferenceAPI"
|
12 |
+
>;
|
13 |
+
variant?: "light" | "dark";
|
14 |
+
}
|
15 |
|
16 |
+
let { model, variant = "light" }: Props = $props();
|
17 |
</script>
|
18 |
|
19 |
<div
|
@@ -9,9 +9,13 @@
|
|
9 |
import CarbonEdit from "~icons/carbon/edit";
|
10 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
11 |
|
12 |
-
|
|
|
|
|
13 |
|
14 |
-
let
|
|
|
|
|
15 |
|
16 |
const dispatch = createEventDispatcher<{
|
17 |
deleteConversation: string;
|
@@ -21,7 +25,7 @@
|
|
21 |
|
22 |
<a
|
23 |
data-sveltekit-noscroll
|
24 |
-
|
25 |
confirmDelete = false;
|
26 |
}}
|
27 |
href="{base}/conversation/{conv.id}"
|
@@ -44,7 +48,7 @@
|
|
44 |
{:else if conv.assistantId}
|
45 |
<div
|
46 |
class="mr-1.5 flex size-4 flex-none items-center justify-center rounded-full bg-gray-300 text-xs font-bold uppercase text-gray-500"
|
47 |
-
|
48 |
{conv.title.replace(/\p{Emoji}/gu, "")}
|
49 |
{:else}
|
50 |
{conv.title}
|
@@ -56,7 +60,10 @@
|
|
56 |
type="button"
|
57 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
58 |
title="Cancel delete action"
|
59 |
-
|
|
|
|
|
|
|
60 |
>
|
61 |
<CarbonClose class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
62 |
</button>
|
@@ -64,7 +71,8 @@
|
|
64 |
type="button"
|
65 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
66 |
title="Confirm delete action"
|
67 |
-
|
|
|
68 |
confirmDelete = false;
|
69 |
dispatch("deleteConversation", conv.id);
|
70 |
}}
|
@@ -76,7 +84,8 @@
|
|
76 |
type="button"
|
77 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
78 |
title="Edit conversation title"
|
79 |
-
|
|
|
80 |
const newTitle = prompt("Edit this conversation title:", conv.title);
|
81 |
if (!newTitle) return;
|
82 |
dispatch("editConversationTitle", { id: conv.id, title: newTitle });
|
@@ -89,7 +98,8 @@
|
|
89 |
type="button"
|
90 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
91 |
title="Delete conversation"
|
92 |
-
|
|
|
93 |
if (event.shiftKey) {
|
94 |
dispatch("deleteConversation", conv.id);
|
95 |
} else {
|
|
|
9 |
import CarbonEdit from "~icons/carbon/edit";
|
10 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
11 |
|
12 |
+
interface Props {
|
13 |
+
conv: ConvSidebar;
|
14 |
+
}
|
15 |
|
16 |
+
let { conv }: Props = $props();
|
17 |
+
|
18 |
+
let confirmDelete = $state(false);
|
19 |
|
20 |
const dispatch = createEventDispatcher<{
|
21 |
deleteConversation: string;
|
|
|
25 |
|
26 |
<a
|
27 |
data-sveltekit-noscroll
|
28 |
+
onmouseleave={() => {
|
29 |
confirmDelete = false;
|
30 |
}}
|
31 |
href="{base}/conversation/{conv.id}"
|
|
|
48 |
{:else if conv.assistantId}
|
49 |
<div
|
50 |
class="mr-1.5 flex size-4 flex-none items-center justify-center rounded-full bg-gray-300 text-xs font-bold uppercase text-gray-500"
|
51 |
+
></div>
|
52 |
{conv.title.replace(/\p{Emoji}/gu, "")}
|
53 |
{:else}
|
54 |
{conv.title}
|
|
|
60 |
type="button"
|
61 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
62 |
title="Cancel delete action"
|
63 |
+
onclick={(e) => {
|
64 |
+
e.preventDefault();
|
65 |
+
confirmDelete = false;
|
66 |
+
}}
|
67 |
>
|
68 |
<CarbonClose class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
|
69 |
</button>
|
|
|
71 |
type="button"
|
72 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
73 |
title="Confirm delete action"
|
74 |
+
onclick={(e) => {
|
75 |
+
e.preventDefault();
|
76 |
confirmDelete = false;
|
77 |
dispatch("deleteConversation", conv.id);
|
78 |
}}
|
|
|
84 |
type="button"
|
85 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
86 |
title="Edit conversation title"
|
87 |
+
onclick={(e) => {
|
88 |
+
e.preventDefault();
|
89 |
const newTitle = prompt("Edit this conversation title:", conv.title);
|
90 |
if (!newTitle) return;
|
91 |
dispatch("editConversationTitle", { id: conv.id, title: newTitle });
|
|
|
98 |
type="button"
|
99 |
class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
|
100 |
title="Delete conversation"
|
101 |
+
onclick={(event) => {
|
102 |
+
event.preventDefault();
|
103 |
if (event.shiftKey) {
|
104 |
dispatch("deleteConversation", conv.id);
|
105 |
} else {
|
@@ -14,13 +14,16 @@
|
|
14 |
import type { Conversation } from "$lib/types/Conversation";
|
15 |
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
20 |
|
21 |
-
|
22 |
|
23 |
-
let hasMore = true;
|
24 |
|
25 |
function handleNewChatClick() {
|
26 |
isAborted.set(true);
|
@@ -32,7 +35,7 @@
|
|
32 |
new Date().setMonth(new Date().getMonth() - 1),
|
33 |
];
|
34 |
|
35 |
-
|
36 |
today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
|
37 |
week: conversations.filter(
|
38 |
({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
|
@@ -41,7 +44,7 @@
|
|
41 |
({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
|
42 |
),
|
43 |
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
|
44 |
-
};
|
45 |
|
46 |
const titles: { [key: string]: string } = {
|
47 |
today: "Today",
|
@@ -73,11 +76,13 @@
|
|
73 |
conversations = [...conversations, ...newConvs];
|
74 |
}
|
75 |
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
81 |
</script>
|
82 |
|
83 |
<div class="sticky top-0 flex flex-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0">
|
@@ -88,13 +93,15 @@
|
|
88 |
<Logo classNames="mr-1" />
|
89 |
{envPublic.PUBLIC_APP_NAME}
|
90 |
</a>
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
98 |
</div>
|
99 |
<div
|
100 |
class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
|
@@ -103,9 +110,9 @@
|
|
103 |
{#if $page.data.nConversations > 0}
|
104 |
<div class="overflow-y-hidden">
|
105 |
<div class="flex animate-pulse flex-col gap-4">
|
106 |
-
<div class="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700"
|
107 |
{#each Array(100) as _}
|
108 |
-
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"
|
109 |
{/each}
|
110 |
</div>
|
111 |
</div>
|
@@ -162,7 +169,7 @@
|
|
162 |
</form>
|
163 |
{/if}
|
164 |
<button
|
165 |
-
|
166 |
type="button"
|
167 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
168 |
>
|
|
|
14 |
import type { Conversation } from "$lib/types/Conversation";
|
15 |
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
|
16 |
|
17 |
+
interface Props {
|
18 |
+
conversations: ConvSidebar[];
|
19 |
+
canLogin: boolean;
|
20 |
+
user: LayoutData["user"];
|
21 |
+
p?: number;
|
22 |
+
}
|
23 |
|
24 |
+
let { conversations = $bindable(), canLogin, user, p = $bindable(0) }: Props = $props();
|
25 |
|
26 |
+
let hasMore = $state(true);
|
27 |
|
28 |
function handleNewChatClick() {
|
29 |
isAborted.set(true);
|
|
|
35 |
new Date().setMonth(new Date().getMonth() - 1),
|
36 |
];
|
37 |
|
38 |
+
let groupedConversations = $derived({
|
39 |
today: conversations.filter(({ updatedAt }) => updatedAt.getTime() > dateRanges[0]),
|
40 |
week: conversations.filter(
|
41 |
({ updatedAt }) => updatedAt.getTime() > dateRanges[1] && updatedAt.getTime() < dateRanges[0]
|
|
|
44 |
({ updatedAt }) => updatedAt.getTime() > dateRanges[2] && updatedAt.getTime() < dateRanges[1]
|
45 |
),
|
46 |
older: conversations.filter(({ updatedAt }) => updatedAt.getTime() < dateRanges[2]),
|
47 |
+
});
|
48 |
|
49 |
const titles: { [key: string]: string } = {
|
50 |
today: "Today",
|
|
|
76 |
conversations = [...conversations, ...newConvs];
|
77 |
}
|
78 |
|
79 |
+
$effect(() => {
|
80 |
+
if (conversations.length <= CONV_NUM_PER_PAGE) {
|
81 |
+
// reset p to 0 if there's only one page of content
|
82 |
+
// that would be caused by a data loading invalidation
|
83 |
+
p = 0;
|
84 |
+
}
|
85 |
+
});
|
86 |
</script>
|
87 |
|
88 |
<div class="sticky top-0 flex flex-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0">
|
|
|
93 |
<Logo classNames="mr-1" />
|
94 |
{envPublic.PUBLIC_APP_NAME}
|
95 |
</a>
|
96 |
+
{#if $page.url.pathname !== base + "/"}
|
97 |
+
<a
|
98 |
+
href={`${base}/`}
|
99 |
+
onclick={handleNewChatClick}
|
100 |
+
class="flex rounded-lg border bg-white px-2 py-0.5 text-center shadow-sm hover:shadow-none dark:border-gray-600 dark:bg-gray-700 sm:text-smd"
|
101 |
+
>
|
102 |
+
New Chat
|
103 |
+
</a>
|
104 |
+
{/if}
|
105 |
</div>
|
106 |
<div
|
107 |
class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl from-gray-50 px-3 pb-3 pt-2 text-[.9rem] dark:from-gray-800/30 max-sm:bg-gradient-to-t md:bg-gradient-to-l"
|
|
|
110 |
{#if $page.data.nConversations > 0}
|
111 |
<div class="overflow-y-hidden">
|
112 |
<div class="flex animate-pulse flex-col gap-4">
|
113 |
+
<div class="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700"></div>
|
114 |
{#each Array(100) as _}
|
115 |
+
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700"></div>
|
116 |
{/each}
|
117 |
</div>
|
118 |
</div>
|
|
|
169 |
</form>
|
170 |
{/if}
|
171 |
<button
|
172 |
+
onclick={switchTheme}
|
173 |
type="button"
|
174 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
175 |
>
|
@@ -10,16 +10,22 @@
|
|
10 |
import IconInternet from "./icons/IconInternet.svelte";
|
11 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
12 |
|
13 |
-
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
(update) => update.subtype === MessageWebSearchUpdateType.Error
|
21 |
);
|
22 |
-
|
|
|
|
|
|
|
23 |
</script>
|
24 |
|
25 |
<details
|
@@ -80,7 +86,7 @@
|
|
80 |
class="-ml-1.5 h-3 w-3 flex-none rounded-full bg-gray-200 dark:bg-gray-600 {loading
|
81 |
? 'group-last:animate-pulse group-last:bg-gray-300 group-last:dark:bg-gray-500'
|
82 |
: ''}"
|
83 |
-
|
84 |
<h3 class="text-md -mt-1.5 pl-2.5 text-gray-800 dark:text-gray-100">
|
85 |
{message.message}
|
86 |
</h3>
|
|
|
10 |
import IconInternet from "./icons/IconInternet.svelte";
|
11 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
12 |
|
13 |
+
interface Props {
|
14 |
+
webSearchMessages?: MessageWebSearchUpdate[];
|
15 |
+
}
|
16 |
+
|
17 |
+
let { webSearchMessages = [] }: Props = $props();
|
18 |
|
19 |
+
let sources = $derived(webSearchMessages.find(isMessageWebSearchSourcesUpdate)?.sources);
|
20 |
+
let lastMessage = $derived(
|
21 |
+
webSearchMessages
|
22 |
+
.filter((update) => update.subtype !== MessageWebSearchUpdateType.Sources)
|
23 |
+
.at(-1) as MessageWebSearchUpdate
|
|
|
24 |
);
|
25 |
+
let errored = $derived(
|
26 |
+
webSearchMessages.some((update) => update.subtype === MessageWebSearchUpdateType.Error)
|
27 |
+
);
|
28 |
+
let loading = $derived(!sources && !errored);
|
29 |
</script>
|
30 |
|
31 |
<details
|
|
|
86 |
class="-ml-1.5 h-3 w-3 flex-none rounded-full bg-gray-200 dark:bg-gray-600 {loading
|
87 |
? 'group-last:animate-pulse group-last:bg-gray-300 group-last:dark:bg-gray-500'
|
88 |
: ''}"
|
89 |
+
></div>
|
90 |
<h3 class="text-md -mt-1.5 pl-2.5 text-gray-800 dark:text-gray-100">
|
91 |
{message.message}
|
92 |
</h3>
|
@@ -3,15 +3,15 @@
|
|
3 |
import { getHref } from "$lib/utils/getHref";
|
4 |
import PaginationArrow from "./PaginationArrow.svelte";
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
|
|
9 |
|
10 |
-
|
11 |
|
12 |
-
|
13 |
-
$: pageIndex = parseInt($page.url.searchParams.get("p") ?? "0");
|
14 |
-
$: pageIndexes = getPageIndexes(pageIndex, numTotalPages);
|
15 |
|
16 |
function getPageIndexes(pageIdx: number, nTotalPages: number) {
|
17 |
let pageIdxs: number[] = [];
|
@@ -52,6 +52,9 @@
|
|
52 |
}
|
53 |
return pageIdxs;
|
54 |
}
|
|
|
|
|
|
|
55 |
</script>
|
56 |
|
57 |
{#if numTotalPages > 1}
|
|
|
3 |
import { getHref } from "$lib/utils/getHref";
|
4 |
import PaginationArrow from "./PaginationArrow.svelte";
|
5 |
|
6 |
+
interface Props {
|
7 |
+
classNames?: string;
|
8 |
+
numItemsPerPage: number;
|
9 |
+
numTotalItems: number;
|
10 |
+
}
|
11 |
|
12 |
+
let { classNames = "", numItemsPerPage, numTotalItems }: Props = $props();
|
13 |
|
14 |
+
const ELLIPSIS_IDX = -1 as const;
|
|
|
|
|
15 |
|
16 |
function getPageIndexes(pageIdx: number, nTotalPages: number) {
|
17 |
let pageIdxs: number[] = [];
|
|
|
52 |
}
|
53 |
return pageIdxs;
|
54 |
}
|
55 |
+
let numTotalPages = $derived(Math.ceil(numTotalItems / numItemsPerPage));
|
56 |
+
let pageIndex = $derived(parseInt($page.url.searchParams.get("p") ?? "0"));
|
57 |
+
let pageIndexes = $derived(getPageIndexes(pageIndex, numTotalPages));
|
58 |
</script>
|
59 |
|
60 |
{#if numTotalPages > 1}
|
@@ -2,9 +2,13 @@
|
|
2 |
import CarbonCaretLeft from "~icons/carbon/caret-left";
|
3 |
import CarbonCaretRight from "~icons/carbon/caret-right";
|
4 |
|
5 |
-
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
8 |
</script>
|
9 |
|
10 |
<a
|
|
|
2 |
import CarbonCaretLeft from "~icons/carbon/caret-left";
|
3 |
import CarbonCaretRight from "~icons/carbon/caret-right";
|
4 |
|
5 |
+
interface Props {
|
6 |
+
href: string;
|
7 |
+
direction: "next" | "previous";
|
8 |
+
isDisabled?: boolean;
|
9 |
+
}
|
10 |
+
|
11 |
+
let { href, direction, isDisabled = false }: Props = $props();
|
12 |
</script>
|
13 |
|
14 |
<a
|
@@ -1,10 +1,15 @@
|
|
1 |
<script lang="ts">
|
2 |
import { onMount, onDestroy } from "svelte";
|
|
|
|
|
|
|
3 |
|
4 |
-
let
|
|
|
|
|
5 |
|
6 |
onMount(() => {
|
7 |
-
el
|
8 |
});
|
9 |
|
10 |
onDestroy(() => {
|
@@ -15,5 +20,5 @@
|
|
15 |
</script>
|
16 |
|
17 |
<div bind:this={el} class="contents" hidden>
|
18 |
-
|
19 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import { onMount, onDestroy } from "svelte";
|
3 |
+
interface Props {
|
4 |
+
children?: import("svelte").Snippet;
|
5 |
+
}
|
6 |
|
7 |
+
let { children }: Props = $props();
|
8 |
+
|
9 |
+
let el: HTMLElement | undefined = $state();
|
10 |
|
11 |
onMount(() => {
|
12 |
+
el?.ownerDocument.body.appendChild(el);
|
13 |
});
|
14 |
|
15 |
onDestroy(() => {
|
|
|
20 |
</script>
|
21 |
|
22 |
<div bind:this={el} class="contents" hidden>
|
23 |
+
{@render children?.()}
|
24 |
</div>
|
@@ -1,12 +1,17 @@
|
|
1 |
<script lang="ts">
|
2 |
import CarbonRotate360 from "~icons/carbon/rotate-360";
|
3 |
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
5 |
</script>
|
6 |
|
7 |
<button
|
8 |
type="button"
|
9 |
-
|
10 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
|
11 |
>
|
12 |
<CarbonRotate360 class="mr-2 text-xs " /> Retry
|
|
|
1 |
<script lang="ts">
|
2 |
import CarbonRotate360 from "~icons/carbon/rotate-360";
|
3 |
|
4 |
+
interface Props {
|
5 |
+
classNames?: string;
|
6 |
+
onClick?: () => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
let { classNames = "", onClick }: Props = $props();
|
10 |
</script>
|
11 |
|
12 |
<button
|
13 |
type="button"
|
14 |
+
onclick={onClick}
|
15 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 text-gray-500 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 {classNames}"
|
16 |
>
|
17 |
<CarbonRotate360 class="mr-2 text-xs " /> Retry
|
@@ -1,27 +1,19 @@
|
|
1 |
<script lang="ts">
|
|
|
|
|
2 |
import { fade } from "svelte/transition";
|
3 |
import { onDestroy } from "svelte";
|
4 |
import IconChevron from "./icons/IconChevron.svelte";
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
let className = "";
|
11 |
-
let observer: ResizeObserver | null = null;
|
12 |
-
|
13 |
-
$: if (scrollNode) {
|
14 |
-
destroy();
|
15 |
-
|
16 |
-
if (window.ResizeObserver) {
|
17 |
-
observer = new ResizeObserver(() => {
|
18 |
-
updateVisibility();
|
19 |
-
});
|
20 |
-
observer.observe(scrollNode);
|
21 |
-
}
|
22 |
-
scrollNode.addEventListener("scroll", updateVisibility);
|
23 |
}
|
24 |
|
|
|
|
|
|
|
25 |
function updateVisibility() {
|
26 |
if (!scrollNode) return;
|
27 |
visible =
|
@@ -34,12 +26,25 @@
|
|
34 |
}
|
35 |
|
36 |
onDestroy(destroy);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
</script>
|
38 |
|
39 |
{#if visible}
|
40 |
<button
|
41 |
transition:fade={{ duration: 150 }}
|
42 |
-
|
43 |
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
|
44 |
><IconChevron classNames="mt-[2px]" /></button
|
45 |
>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { run } from "svelte/legacy";
|
3 |
+
|
4 |
import { fade } from "svelte/transition";
|
5 |
import { onDestroy } from "svelte";
|
6 |
import IconChevron from "./icons/IconChevron.svelte";
|
7 |
|
8 |
+
let visible = $state(false);
|
9 |
+
interface Props {
|
10 |
+
scrollNode: HTMLElement;
|
11 |
+
class?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
}
|
13 |
|
14 |
+
let { scrollNode, class: className = "" }: Props = $props();
|
15 |
+
let observer: ResizeObserver | null = $state(null);
|
16 |
+
|
17 |
function updateVisibility() {
|
18 |
if (!scrollNode) return;
|
19 |
visible =
|
|
|
26 |
}
|
27 |
|
28 |
onDestroy(destroy);
|
29 |
+
run(() => {
|
30 |
+
if (scrollNode) {
|
31 |
+
destroy();
|
32 |
+
|
33 |
+
if (window.ResizeObserver) {
|
34 |
+
observer = new ResizeObserver(() => {
|
35 |
+
updateVisibility();
|
36 |
+
});
|
37 |
+
observer.observe(scrollNode);
|
38 |
+
}
|
39 |
+
scrollNode.addEventListener("scroll", updateVisibility);
|
40 |
+
}
|
41 |
+
});
|
42 |
</script>
|
43 |
|
44 |
{#if visible}
|
45 |
<button
|
46 |
transition:fade={{ duration: 150 }}
|
47 |
+
onclick={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth" })}
|
48 |
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
|
49 |
><IconChevron classNames="mt-[2px]" /></button
|
50 |
>
|
@@ -1,27 +1,17 @@
|
|
1 |
<script lang="ts">
|
2 |
import { fade } from "svelte/transition";
|
3 |
-
import { onDestroy } from "svelte";
|
4 |
import IconChevron from "./icons/IconChevron.svelte";
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
let className = "";
|
11 |
-
let observer: ResizeObserver | null = null;
|
12 |
-
|
13 |
-
$: if (scrollNode) {
|
14 |
-
destroy();
|
15 |
-
|
16 |
-
if (window.ResizeObserver) {
|
17 |
-
observer = new ResizeObserver(() => {
|
18 |
-
updateVisibility();
|
19 |
-
});
|
20 |
-
observer.observe(scrollNode);
|
21 |
-
}
|
22 |
-
scrollNode.addEventListener("scroll", updateVisibility);
|
23 |
}
|
24 |
|
|
|
|
|
|
|
25 |
function updateVisibility() {
|
26 |
if (!scrollNode) return;
|
27 |
visible =
|
@@ -57,12 +47,29 @@
|
|
57 |
}
|
58 |
|
59 |
onDestroy(destroy);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
</script>
|
61 |
|
62 |
{#if visible}
|
63 |
<button
|
64 |
transition:fade={{ duration: 150 }}
|
65 |
-
|
66 |
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
|
67 |
>
|
68 |
<IconChevron classNames="rotate-180 mt-[2px]" />
|
|
|
1 |
<script lang="ts">
|
2 |
import { fade } from "svelte/transition";
|
3 |
+
import { onDestroy, untrack } from "svelte";
|
4 |
import IconChevron from "./icons/IconChevron.svelte";
|
5 |
|
6 |
+
let visible = $state(false);
|
7 |
+
interface Props {
|
8 |
+
scrollNode: HTMLElement;
|
9 |
+
class?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
}
|
11 |
|
12 |
+
let { scrollNode, class: className = "" }: Props = $props();
|
13 |
+
let observer: ResizeObserver | null = $state(null);
|
14 |
+
|
15 |
function updateVisibility() {
|
16 |
if (!scrollNode) return;
|
17 |
visible =
|
|
|
47 |
}
|
48 |
|
49 |
onDestroy(destroy);
|
50 |
+
|
51 |
+
$effect(() => {
|
52 |
+
scrollNode &&
|
53 |
+
untrack(() => {
|
54 |
+
if (scrollNode) {
|
55 |
+
destroy();
|
56 |
+
|
57 |
+
if (window.ResizeObserver) {
|
58 |
+
observer = new ResizeObserver(() => {
|
59 |
+
updateVisibility();
|
60 |
+
});
|
61 |
+
observer.observe(scrollNode);
|
62 |
+
}
|
63 |
+
scrollNode.addEventListener("scroll", updateVisibility);
|
64 |
+
}
|
65 |
+
});
|
66 |
+
});
|
67 |
</script>
|
68 |
|
69 |
{#if visible}
|
70 |
<button
|
71 |
transition:fade={{ duration: 150 }}
|
72 |
+
onclick={scrollToPrevious}
|
73 |
class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
|
74 |
>
|
75 |
<IconChevron classNames="rotate-180 mt-[2px]" />
|
@@ -1,12 +1,17 @@
|
|
1 |
<script lang="ts">
|
2 |
import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
|
3 |
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
5 |
</script>
|
6 |
|
7 |
<button
|
8 |
type="button"
|
9 |
-
|
10 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 {classNames}"
|
11 |
>
|
12 |
<CarbonStopFilledAlt class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-300" /> Stop generating
|
|
|
1 |
<script lang="ts">
|
2 |
import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
|
3 |
|
4 |
+
interface Props {
|
5 |
+
classNames?: string;
|
6 |
+
onClick?: () => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
let { classNames = "", onClick }: Props = $props();
|
10 |
</script>
|
11 |
|
12 |
<button
|
13 |
type="button"
|
14 |
+
onclick={onClick}
|
15 |
class="btn flex h-8 rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 {classNames}"
|
16 |
>
|
17 |
<CarbonStopFilledAlt class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-300" /> Stop generating
|
@@ -1,6 +1,10 @@
|
|
1 |
<script lang="ts">
|
2 |
-
|
3 |
-
|
|
|
|
|
|
|
|
|
4 |
</script>
|
5 |
|
6 |
<input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
|
@@ -12,5 +16,5 @@
|
|
12 |
tabindex="0"
|
13 |
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
|
14 |
>
|
15 |
-
<div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all"
|
16 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
+
interface Props {
|
3 |
+
checked: boolean;
|
4 |
+
name: string;
|
5 |
+
}
|
6 |
+
|
7 |
+
let { checked = $bindable(), name }: Props = $props();
|
8 |
</script>
|
9 |
|
10 |
<input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
|
|
|
16 |
tabindex="0"
|
17 |
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
|
18 |
>
|
19 |
+
<div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all"></div>
|
20 |
</div>
|
@@ -3,16 +3,20 @@
|
|
3 |
import CarbonClose from "~icons/carbon/close";
|
4 |
import CarbonBlockchain from "~icons/carbon/blockchain";
|
5 |
|
6 |
-
|
|
|
|
|
7 |
|
8 |
-
let
|
|
|
|
|
9 |
</script>
|
10 |
|
11 |
<button
|
12 |
type="button"
|
13 |
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 px-3 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
14 |
-
|
15 |
-
|
16 |
>
|
17 |
<CarbonBlockchain class="text-xxs" /> Using Custom System Prompt
|
18 |
</button>
|
@@ -22,7 +26,7 @@
|
|
22 |
<div class="flex w-full flex-col gap-5 p-6">
|
23 |
<div class="flex items-start justify-between text-xl font-semibold text-gray-800">
|
24 |
<h2>System Prompt</h2>
|
25 |
-
<button type="button" class="group"
|
26 |
<CarbonClose class="mt-auto text-gray-900 group-hover:text-gray-500" />
|
27 |
</button>
|
28 |
</div>
|
@@ -30,7 +34,7 @@
|
|
30 |
disabled
|
31 |
value={preprompt}
|
32 |
class="min-h-[420px] w-full resize-none rounded-lg border bg-gray-50 p-2.5 text-gray-600 max-sm:text-sm"
|
33 |
-
|
34 |
</div>
|
35 |
</Modal>
|
36 |
{/if}
|
|
|
3 |
import CarbonClose from "~icons/carbon/close";
|
4 |
import CarbonBlockchain from "~icons/carbon/blockchain";
|
5 |
|
6 |
+
interface Props {
|
7 |
+
preprompt: string;
|
8 |
+
}
|
9 |
|
10 |
+
let { preprompt }: Props = $props();
|
11 |
+
|
12 |
+
let isOpen = $state(false);
|
13 |
</script>
|
14 |
|
15 |
<button
|
16 |
type="button"
|
17 |
class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 px-3 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
18 |
+
onclick={() => (isOpen = !isOpen)}
|
19 |
+
onkeydown={(e) => e.key === "Enter" && (isOpen = !isOpen)}
|
20 |
>
|
21 |
<CarbonBlockchain class="text-xxs" /> Using Custom System Prompt
|
22 |
</button>
|
|
|
26 |
<div class="flex w-full flex-col gap-5 p-6">
|
27 |
<div class="flex items-start justify-between text-xl font-semibold text-gray-800">
|
28 |
<h2>System Prompt</h2>
|
29 |
+
<button type="button" class="group" onclick={() => (isOpen = false)}>
|
30 |
<CarbonClose class="mt-auto text-gray-900 group-hover:text-gray-500" />
|
31 |
</button>
|
32 |
</div>
|
|
|
34 |
disabled
|
35 |
value={preprompt}
|
36 |
class="min-h-[420px] w-full resize-none rounded-lg border bg-gray-50 p-2.5 text-gray-600 max-sm:text-sm"
|
37 |
+
></textarea>
|
38 |
</div>
|
39 |
</Modal>
|
40 |
{/if}
|
@@ -3,7 +3,11 @@
|
|
3 |
|
4 |
import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
|
5 |
|
6 |
-
|
|
|
|
|
|
|
|
|
7 |
</script>
|
8 |
|
9 |
<div
|
|
|
3 |
|
4 |
import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
|
5 |
|
6 |
+
interface Props {
|
7 |
+
message?: string;
|
8 |
+
}
|
9 |
+
|
10 |
+
let { message = "" }: Props = $props();
|
11 |
</script>
|
12 |
|
13 |
<div
|
@@ -2,43 +2,43 @@
|
|
2 |
import type { Model } from "$lib/types/Model";
|
3 |
import { getTokenizer } from "$lib/utils/getTokenizer";
|
4 |
import type { PreTrainedTokenizer } from "@huggingface/transformers";
|
|
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
10 |
|
11 |
-
let
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
-
|
22 |
-
tokenizer = await getTokenizer(modelTokenizer);
|
23 |
-
})();
|
24 |
</script>
|
25 |
|
26 |
-
{
|
27 |
-
|
28 |
-
{
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
class="invisible absolute -top-6 right-0 whitespace-nowrap rounded bg-black px-1 text-sm text-white peer-hover:visible"
|
39 |
-
>
|
40 |
-
Tokens usage
|
41 |
-
</div>
|
42 |
-
</div>
|
43 |
-
{/await}
|
44 |
-
{/if}
|
|
|
2 |
import type { Model } from "$lib/types/Model";
|
3 |
import { getTokenizer } from "$lib/utils/getTokenizer";
|
4 |
import type { PreTrainedTokenizer } from "@huggingface/transformers";
|
5 |
+
import { untrack } from "svelte";
|
6 |
|
7 |
+
interface Props {
|
8 |
+
classNames?: string;
|
9 |
+
prompt?: string;
|
10 |
+
modelTokenizer: Exclude<Model["tokenizer"], undefined>;
|
11 |
+
truncate?: number | undefined;
|
12 |
+
}
|
13 |
|
14 |
+
let { classNames = "", prompt = "", modelTokenizer, truncate = undefined }: Props = $props();
|
15 |
|
16 |
+
let tokenizer: Promise<PreTrainedTokenizer> = $derived(getTokenizer(modelTokenizer));
|
17 |
+
|
18 |
+
let nTokens = $state(0);
|
19 |
+
|
20 |
+
$effect(() => {
|
21 |
+
prompt &&
|
22 |
+
untrack(() => {
|
23 |
+
tokenizer.then((tokenizer) => {
|
24 |
+
const { input_ids } = tokenizer(prompt);
|
25 |
+
nTokens = input_ids.size;
|
26 |
+
});
|
27 |
+
});
|
28 |
+
});
|
29 |
|
30 |
+
let exceedLimit = $derived(nTokens > (truncate || Infinity));
|
|
|
|
|
31 |
</script>
|
32 |
|
33 |
+
<div class={classNames}>
|
34 |
+
<p
|
35 |
+
class="peer text-sm {exceedLimit ? 'text-red-500 opacity-100' : 'opacity-60 hover:opacity-90'}"
|
36 |
+
>
|
37 |
+
{nTokens}{truncate ? `/${truncate}` : ""}
|
38 |
+
</p>
|
39 |
+
<div
|
40 |
+
class="invisible absolute -top-6 right-0 whitespace-nowrap rounded bg-black px-1 text-sm text-white peer-hover:visible"
|
41 |
+
>
|
42 |
+
Tokens usage
|
43 |
+
</div>
|
44 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -3,7 +3,11 @@
|
|
3 |
import { base } from "$app/paths";
|
4 |
import { browser } from "$app/environment";
|
5 |
|
6 |
-
|
|
|
|
|
|
|
|
|
7 |
</script>
|
8 |
|
9 |
<div
|
@@ -11,7 +15,9 @@
|
|
11 |
>
|
12 |
{#if browser}
|
13 |
{#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value}
|
14 |
-
|
|
|
|
|
15 |
<div class="flex flex-col items-center justify-center py-1">
|
16 |
<a
|
17 |
href={`${base}/tools/${value._id}`}
|
|
|
3 |
import { base } from "$app/paths";
|
4 |
import { browser } from "$app/environment";
|
5 |
|
6 |
+
interface Props {
|
7 |
+
toolId: string;
|
8 |
+
}
|
9 |
+
|
10 |
+
let { toolId }: Props = $props();
|
11 |
</script>
|
12 |
|
13 |
<div
|
|
|
15 |
>
|
16 |
{#if browser}
|
17 |
{#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value}
|
18 |
+
{#key value.color + value.icon}
|
19 |
+
<ToolLogo color={value.color} icon={value.icon} size="sm" />
|
20 |
+
{/key}
|
21 |
<div class="flex flex-col items-center justify-center py-1">
|
22 |
<a
|
23 |
href={`${base}/tools/${value._id}`}
|
@@ -11,28 +11,34 @@
|
|
11 |
import CarbonSpeaker from "~icons/carbon/volume-up";
|
12 |
import CarbonVideo from "~icons/carbon/video";
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
34 |
|
35 |
-
let iconEl = CarbonWikis;
|
36 |
|
37 |
switch (icon) {
|
38 |
case "wikis":
|
@@ -70,18 +76,22 @@
|
|
70 |
break;
|
71 |
}
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
|
|
85 |
</script>
|
86 |
|
87 |
<div class="flex {sizeClass} relative items-center justify-center">
|
@@ -100,5 +110,5 @@
|
|
100 |
</defs>
|
101 |
<rect width="100%" height="100%" fill="url(#gradient-{gradientColor})" mask="url(#mask)" />
|
102 |
</svg>
|
103 |
-
<
|
104 |
</div>
|
|
|
11 |
import CarbonSpeaker from "~icons/carbon/volume-up";
|
12 |
import CarbonVideo from "~icons/carbon/video";
|
13 |
|
14 |
+
interface Props {
|
15 |
+
color: string;
|
16 |
+
icon: string;
|
17 |
+
size?: "xs" | "sm" | "md" | "lg";
|
18 |
+
}
|
19 |
+
|
20 |
+
let { color, icon, size = "md" }: Props = $props();
|
21 |
|
22 |
+
let gradientColor = $derived(
|
23 |
+
(() => {
|
24 |
+
switch (color) {
|
25 |
+
case "purple":
|
26 |
+
return "#653789";
|
27 |
+
case "blue":
|
28 |
+
return "#375889";
|
29 |
+
case "green":
|
30 |
+
return "#37894E";
|
31 |
+
case "yellow":
|
32 |
+
return "#897C37";
|
33 |
+
case "red":
|
34 |
+
return "#893737";
|
35 |
+
default:
|
36 |
+
return "#FFF";
|
37 |
+
}
|
38 |
+
})()
|
39 |
+
);
|
40 |
|
41 |
+
let iconEl = $state(CarbonWikis);
|
42 |
|
43 |
switch (icon) {
|
44 |
case "wikis":
|
|
|
76 |
break;
|
77 |
}
|
78 |
|
79 |
+
let sizeClass = $derived(
|
80 |
+
(() => {
|
81 |
+
switch (size) {
|
82 |
+
case "xs":
|
83 |
+
return "size-4";
|
84 |
+
case "sm":
|
85 |
+
return "size-8";
|
86 |
+
case "md":
|
87 |
+
return "size-14";
|
88 |
+
case "lg":
|
89 |
+
return "size-24";
|
90 |
+
}
|
91 |
+
})()
|
92 |
+
);
|
93 |
+
|
94 |
+
const SvelteComponent = $derived(iconEl);
|
95 |
</script>
|
96 |
|
97 |
<div class="flex {sizeClass} relative items-center justify-center">
|
|
|
110 |
</defs>
|
111 |
<rect width="100%" height="100%" fill="url(#gradient-{gradientColor})" mask="url(#mask)" />
|
112 |
</svg>
|
113 |
+
<SvelteComponent class="relative {sizeClass} scale-50 text-clip text-gray-200" />
|
114 |
</div>
|
@@ -9,17 +9,23 @@
|
|
9 |
import CarbonInformation from "~icons/carbon/information";
|
10 |
import CarbonGlobe from "~icons/carbon/earth-filled";
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
13 |
const settings = useSettingsStore();
|
14 |
|
15 |
-
let detailsEl: HTMLDetailsElement;
|
16 |
|
17 |
// active tools are all the checked tools, either from settings or on by default
|
18 |
-
|
19 |
-
(
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
23 |
|
24 |
async function setAllTools(value: boolean) {
|
25 |
const configToolsIds = $page.data.tools
|
@@ -37,16 +43,16 @@
|
|
37 |
}
|
38 |
}
|
39 |
|
40 |
-
|
41 |
|
42 |
-
|
43 |
</script>
|
44 |
|
45 |
<details
|
46 |
class="group relative bottom-0 h-full min-h-8"
|
47 |
bind:this={detailsEl}
|
48 |
use:clickOutside={() => {
|
49 |
-
if (detailsEl
|
50 |
detailsEl.removeAttribute("open");
|
51 |
}
|
52 |
}}
|
@@ -75,7 +81,10 @@
|
|
75 |
{/if}
|
76 |
<button
|
77 |
class="ml-auto text-xs underline"
|
78 |
-
|
|
|
|
|
|
|
79 |
>
|
80 |
{#if allToolsEnabled}
|
81 |
Disable all
|
@@ -104,7 +113,9 @@
|
|
104 |
id={tool._id}
|
105 |
checked={true}
|
106 |
class="rounded-xs font-semibold accent-purple-500 hover:accent-purple-600"
|
107 |
-
|
|
|
|
|
108 |
await settings.instantSet({
|
109 |
tools: $settings?.tools?.filter((t) => t !== tool._id) ?? [],
|
110 |
});
|
@@ -116,7 +127,9 @@
|
|
116 |
id={tool._id}
|
117 |
checked={isChecked}
|
118 |
disabled={loading}
|
119 |
-
|
|
|
|
|
120 |
if (isChecked) {
|
121 |
await settings.instantSet({
|
122 |
tools: ($settings?.tools ?? []).filter((t) => t !== tool._id),
|
|
|
9 |
import CarbonInformation from "~icons/carbon/information";
|
10 |
import CarbonGlobe from "~icons/carbon/earth-filled";
|
11 |
|
12 |
+
interface Props {
|
13 |
+
loading?: boolean;
|
14 |
+
}
|
15 |
+
|
16 |
+
let { loading = false }: Props = $props();
|
17 |
const settings = useSettingsStore();
|
18 |
|
19 |
+
let detailsEl: HTMLDetailsElement | undefined = $state();
|
20 |
|
21 |
// active tools are all the checked tools, either from settings or on by default
|
22 |
+
let activeToolCount = $derived(
|
23 |
+
$page.data.tools.filter(
|
24 |
+
(tool: ToolFront) =>
|
25 |
+
// community tools are always on by default
|
26 |
+
tool.type === "community" || $settings?.tools?.includes(tool._id)
|
27 |
+
).length
|
28 |
+
);
|
29 |
|
30 |
async function setAllTools(value: boolean) {
|
31 |
const configToolsIds = $page.data.tools
|
|
|
43 |
}
|
44 |
}
|
45 |
|
46 |
+
let allToolsEnabled = $derived(activeToolCount === $page.data.tools.length);
|
47 |
|
48 |
+
let tools = $derived($page.data.tools);
|
49 |
</script>
|
50 |
|
51 |
<details
|
52 |
class="group relative bottom-0 h-full min-h-8"
|
53 |
bind:this={detailsEl}
|
54 |
use:clickOutside={() => {
|
55 |
+
if (detailsEl?.hasAttribute("open")) {
|
56 |
detailsEl.removeAttribute("open");
|
57 |
}
|
58 |
}}
|
|
|
81 |
{/if}
|
82 |
<button
|
83 |
class="ml-auto text-xs underline"
|
84 |
+
onclick={(e) => {
|
85 |
+
e.stopPropagation();
|
86 |
+
setAllTools(!allToolsEnabled);
|
87 |
+
}}
|
88 |
>
|
89 |
{#if allToolsEnabled}
|
90 |
Disable all
|
|
|
113 |
id={tool._id}
|
114 |
checked={true}
|
115 |
class="rounded-xs font-semibold accent-purple-500 hover:accent-purple-600"
|
116 |
+
onclick={async (e) => {
|
117 |
+
e.preventDefault();
|
118 |
+
e.stopPropagation();
|
119 |
await settings.instantSet({
|
120 |
tools: $settings?.tools?.filter((t) => t !== tool._id) ?? [],
|
121 |
});
|
|
|
127 |
id={tool._id}
|
128 |
checked={isChecked}
|
129 |
disabled={loading}
|
130 |
+
onclick={async (e) => {
|
131 |
+
e.preventDefault();
|
132 |
+
e.stopPropagation();
|
133 |
if (isChecked) {
|
134 |
await settings.instantSet({
|
135 |
tools: ($settings?.tools ?? []).filter((t) => t !== tool._id),
|
@@ -1,7 +1,15 @@
|
|
1 |
<script lang="ts">
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
</script>
|
6 |
|
7 |
<div
|
@@ -17,6 +25,6 @@
|
|
17 |
border-left-color: transparent;
|
18 |
border-right-color: transparent;
|
19 |
"
|
20 |
-
|
21 |
{label}
|
22 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
+
interface Props {
|
3 |
+
classNames?: string;
|
4 |
+
label?: string;
|
5 |
+
position?: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
let {
|
9 |
+
classNames = "",
|
10 |
+
label = "Copied",
|
11 |
+
position = "left-1/2 top-full transform -translate-x-1/2 translate-y-2",
|
12 |
+
}: Props = $props();
|
13 |
</script>
|
14 |
|
15 |
<div
|
|
|
25 |
border-left-color: transparent;
|
26 |
border-right-color: transparent;
|
27 |
"
|
28 |
+
></div>
|
29 |
{label}
|
30 |
</div>
|
@@ -1,9 +1,13 @@
|
|
1 |
<script lang="ts">
|
2 |
import CarbonUpload from "~icons/carbon/upload";
|
3 |
|
4 |
-
|
5 |
-
|
6 |
-
|
|
|
|
|
|
|
|
|
7 |
|
8 |
/**
|
9 |
* Due to a bug with Svelte, we cannot use bind:files with multiple
|
@@ -23,7 +27,7 @@
|
|
23 |
class="absolute w-full cursor-pointer opacity-0"
|
24 |
aria-label="Upload file"
|
25 |
type="file"
|
26 |
-
|
27 |
accept={mimeTypes.join(",")}
|
28 |
/>
|
29 |
<CarbonUpload class="mr-2 text-xxs" /> Upload file
|
|
|
1 |
<script lang="ts">
|
2 |
import CarbonUpload from "~icons/carbon/upload";
|
3 |
|
4 |
+
interface Props {
|
5 |
+
classNames?: string;
|
6 |
+
files: File[];
|
7 |
+
mimeTypes: string[];
|
8 |
+
}
|
9 |
+
|
10 |
+
let { classNames = "", files = $bindable(), mimeTypes }: Props = $props();
|
11 |
|
12 |
/**
|
13 |
* Due to a bug with Svelte, we cannot use bind:files with multiple
|
|
|
27 |
class="absolute w-full cursor-pointer opacity-0"
|
28 |
aria-label="Upload file"
|
29 |
type="file"
|
30 |
+
onchange={onFileChange}
|
31 |
accept={mimeTypes.join(",")}
|
32 |
/>
|
33 |
<CarbonUpload class="mr-2 text-xxs" /> Upload file
|
@@ -8,8 +8,8 @@
|
|
8 |
|
9 |
<div
|
10 |
class="flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg border bg-white p-1.5 shadow-sm hover:shadow-none dark:border-gray-800 dark:bg-gray-900"
|
11 |
-
|
12 |
-
|
13 |
aria-checked={$webSearchParameters.useSearch}
|
14 |
aria-label="Web Search Toggle"
|
15 |
role="switch"
|
|
|
8 |
|
9 |
<div
|
10 |
class="flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg border bg-white p-1.5 shadow-sm hover:shadow-none dark:border-gray-800 dark:bg-gray-900"
|
11 |
+
onclick={toggle}
|
12 |
+
onkeydown={toggle}
|
13 |
aria-checked={$webSearchParameters.useSearch}
|
14 |
aria-label="Web Search Toggle"
|
15 |
role="switch"
|
@@ -7,11 +7,15 @@
|
|
7 |
import { enhance } from "$app/forms";
|
8 |
import { createEventDispatcher } from "svelte";
|
9 |
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
13 |
|
14 |
-
|
|
|
|
|
15 |
|
16 |
const dispatch = createEventDispatcher<{
|
17 |
showAlternateMsg: { id: Message["id"] };
|
@@ -23,7 +27,7 @@
|
|
23 |
>
|
24 |
<button
|
25 |
class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
|
26 |
-
|
27 |
disabled={currentIdx === 0 || loading}
|
28 |
>
|
29 |
<CarbonChevronLeft class="text-sm" />
|
@@ -33,7 +37,7 @@
|
|
33 |
</span>
|
34 |
<button
|
35 |
class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
|
36 |
-
|
37 |
dispatch("showAlternateMsg", {
|
38 |
id: alternatives[Math.min(alternatives.length - 1, currentIdx + 1)],
|
39 |
})}
|
|
|
7 |
import { enhance } from "$app/forms";
|
8 |
import { createEventDispatcher } from "svelte";
|
9 |
|
10 |
+
interface Props {
|
11 |
+
message: Message;
|
12 |
+
alternatives?: Message["id"][];
|
13 |
+
loading?: boolean;
|
14 |
+
}
|
15 |
|
16 |
+
let { message, alternatives = [], loading = false }: Props = $props();
|
17 |
+
|
18 |
+
let currentIdx = $derived(alternatives.findIndex((id) => id === message.id));
|
19 |
|
20 |
const dispatch = createEventDispatcher<{
|
21 |
showAlternateMsg: { id: Message["id"] };
|
|
|
27 |
>
|
28 |
<button
|
29 |
class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
|
30 |
+
onclick={() => dispatch("showAlternateMsg", { id: alternatives[Math.max(0, currentIdx - 1)] })}
|
31 |
disabled={currentIdx === 0 || loading}
|
32 |
>
|
33 |
<CarbonChevronLeft class="text-sm" />
|
|
|
37 |
</span>
|
38 |
<button
|
39 |
class="inline text-lg font-thin text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:opacity-25 dark:text-gray-500 dark:hover:text-gray-200"
|
40 |
+
onclick={() =>
|
41 |
dispatch("showAlternateMsg", {
|
42 |
id: alternatives[Math.min(alternatives.length - 1, currentIdx + 1)],
|
43 |
})}
|
@@ -18,36 +18,41 @@
|
|
18 |
import { env as envPublic } from "$env/dynamic/public";
|
19 |
import { page } from "$app/stores";
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
36 |
|
37 |
const dispatch = createEventDispatcher<{ message: string }>();
|
38 |
|
39 |
-
|
40 |
assistant?.rag?.allowAllDomains ||
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
44 |
|
45 |
const prefix =
|
46 |
envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || $page.url.origin}${base}`;
|
47 |
|
48 |
-
|
49 |
|
50 |
-
let isCopied = false;
|
51 |
|
52 |
const settings = useSettingsStore();
|
53 |
</script>
|
@@ -128,7 +133,7 @@
|
|
128 |
<div class="flex flex-row items-center gap-1">
|
129 |
<button
|
130 |
class="flex h-7 items-center gap-1.5 rounded-full border bg-white px-2.5 py-1 text-gray-800 shadow-sm hover:shadow-inner dark:border-gray-700 dark:bg-gray-700 dark:text-gray-300/90 dark:hover:bg-gray-800 max-sm:px-1.5 md:text-sm"
|
131 |
-
|
132 |
if (!isCopied) {
|
133 |
share(shareUrl, assistant.name);
|
134 |
isCopied = true;
|
@@ -154,7 +159,7 @@
|
|
154 |
</div>
|
155 |
</div>
|
156 |
<button
|
157 |
-
|
158 |
settings.instantSet({
|
159 |
activeModel: models[0].name,
|
160 |
});
|
@@ -177,7 +182,7 @@
|
|
177 |
<button
|
178 |
type="button"
|
179 |
class="truncate whitespace-nowrap rounded-xl border bg-gray-50 px-3 py-2 text-left text-smd text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
180 |
-
|
181 |
>
|
182 |
{example}
|
183 |
</button>
|
|
|
18 |
import { env as envPublic } from "$env/dynamic/public";
|
19 |
import { page } from "$app/stores";
|
20 |
|
21 |
+
interface Props {
|
22 |
+
models: Model[];
|
23 |
+
assistant: Pick<
|
24 |
+
Assistant,
|
25 |
+
| "avatar"
|
26 |
+
| "name"
|
27 |
+
| "rag"
|
28 |
+
| "dynamicPrompt"
|
29 |
+
| "modelId"
|
30 |
+
| "createdByName"
|
31 |
+
| "exampleInputs"
|
32 |
+
| "_id"
|
33 |
+
| "description"
|
34 |
+
| "userCount"
|
35 |
+
| "tools"
|
36 |
+
>;
|
37 |
+
}
|
38 |
+
|
39 |
+
let { models, assistant }: Props = $props();
|
40 |
|
41 |
const dispatch = createEventDispatcher<{ message: string }>();
|
42 |
|
43 |
+
let hasRag = $derived(
|
44 |
assistant?.rag?.allowAllDomains ||
|
45 |
+
(assistant?.rag?.allowedDomains?.length ?? 0) > 0 ||
|
46 |
+
(assistant?.rag?.allowedLinks?.length ?? 0) > 0 ||
|
47 |
+
assistant?.dynamicPrompt
|
48 |
+
);
|
49 |
|
50 |
const prefix =
|
51 |
envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || $page.url.origin}${base}`;
|
52 |
|
53 |
+
let shareUrl = $derived(`${prefix}/assistant/${assistant?._id}`);
|
54 |
|
55 |
+
let isCopied = $state(false);
|
56 |
|
57 |
const settings = useSettingsStore();
|
58 |
</script>
|
|
|
133 |
<div class="flex flex-row items-center gap-1">
|
134 |
<button
|
135 |
class="flex h-7 items-center gap-1.5 rounded-full border bg-white px-2.5 py-1 text-gray-800 shadow-sm hover:shadow-inner dark:border-gray-700 dark:bg-gray-700 dark:text-gray-300/90 dark:hover:bg-gray-800 max-sm:px-1.5 md:text-sm"
|
136 |
+
onclick={() => {
|
137 |
if (!isCopied) {
|
138 |
share(shareUrl, assistant.name);
|
139 |
isCopied = true;
|
|
|
159 |
</div>
|
160 |
</div>
|
161 |
<button
|
162 |
+
onclick={() => {
|
163 |
settings.instantSet({
|
164 |
activeModel: models[0].name,
|
165 |
});
|
|
|
182 |
<button
|
183 |
type="button"
|
184 |
class="truncate whitespace-nowrap rounded-xl border bg-gray-50 px-3 py-2 text-left text-smd text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
185 |
+
onclick={() => dispatch("message", example)}
|
186 |
>
|
187 |
{example}
|
188 |
</button>
|
@@ -23,18 +23,35 @@
|
|
23 |
import IconAdd from "~icons/carbon/add";
|
24 |
import { captureScreen } from "$lib/utils/screenshot";
|
25 |
import IconScreenshot from "../icons/IconScreenshot.svelte";
|
|
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
35 |
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
const onFileChange = async (e: Event) => {
|
40 |
if (!e.target) return;
|
@@ -48,20 +65,20 @@
|
|
48 |
}
|
49 |
};
|
50 |
|
51 |
-
let textareaElement: HTMLTextAreaElement;
|
52 |
-
let isCompositionOn = false;
|
53 |
|
54 |
const dispatch = createEventDispatcher<{ submit: void }>();
|
55 |
|
56 |
onMount(() => {
|
57 |
if (!isVirtualKeyboard()) {
|
58 |
-
textareaElement
|
59 |
}
|
60 |
function onFormSubmit() {
|
61 |
adjustTextareaHeight();
|
62 |
}
|
63 |
|
64 |
-
const formEl = textareaElement
|
65 |
formEl?.addEventListener("submit", onFormSubmit);
|
66 |
return () => {
|
67 |
formEl?.removeEventListener("submit", onFormSubmit);
|
@@ -84,6 +101,10 @@
|
|
84 |
}
|
85 |
|
86 |
function adjustTextareaHeight() {
|
|
|
|
|
|
|
|
|
87 |
textareaElement.style.height = "auto";
|
88 |
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
89 |
|
@@ -109,24 +130,29 @@
|
|
109 |
|
110 |
// tool section
|
111 |
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
|
|
|
|
117 |
|
118 |
-
|
119 |
-
modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"))
|
|
|
120 |
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
(
|
125 |
-
|
126 |
-
|
|
|
|
|
127 |
</script>
|
128 |
|
129 |
-
<div class="flex min-h-full flex-1 flex-col"
|
130 |
<textarea
|
131 |
rows="1"
|
132 |
tabindex="0"
|
@@ -135,14 +161,19 @@
|
|
135 |
class:text-gray-400={disabled}
|
136 |
bind:value
|
137 |
bind:this={textareaElement}
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
143 |
{placeholder}
|
144 |
{disabled}
|
145 |
-
|
146 |
|
147 |
{#if !assistant}
|
148 |
<div
|
@@ -159,7 +190,8 @@
|
|
159 |
class="base-tool"
|
160 |
class:active-tool={webSearchIsOn}
|
161 |
disabled={loading}
|
162 |
-
|
|
|
163 |
if (modelHasTools) {
|
164 |
if (webSearchIsOn) {
|
165 |
await settings.instantSet({
|
@@ -195,7 +227,8 @@
|
|
195 |
class="base-tool"
|
196 |
class:active-tool={imageGenIsOn}
|
197 |
disabled={loading}
|
198 |
-
|
|
|
199 |
if (modelHasTools) {
|
200 |
if (imageGenIsOn) {
|
201 |
await settings.instantSet({
|
@@ -227,7 +260,7 @@
|
|
227 |
return m.split("/")[1];
|
228 |
})
|
229 |
.join(", ")}
|
230 |
-
<
|
231 |
<HoverTooltip
|
232 |
label={mimeTypesString.includes("*")
|
233 |
? "Upload any file"
|
@@ -241,7 +274,7 @@
|
|
241 |
class="absolute hidden size-0"
|
242 |
aria-label="Upload file"
|
243 |
type="file"
|
244 |
-
|
245 |
accept={mimeTypes.join(",")}
|
246 |
/>
|
247 |
<IconPaperclip classNames="text-xl" />
|
@@ -250,7 +283,7 @@
|
|
250 |
{/if}
|
251 |
</label>
|
252 |
</HoverTooltip>
|
253 |
-
</
|
254 |
{#if mimeTypes.includes("image/*")}
|
255 |
<HoverTooltip
|
256 |
label="Capture screenshot"
|
@@ -259,7 +292,8 @@
|
|
259 |
>
|
260 |
<button
|
261 |
class="base-tool"
|
262 |
-
|
|
|
263 |
const screenshot = await captureScreen();
|
264 |
|
265 |
// Convert base64 to blob
|
@@ -282,11 +316,14 @@
|
|
282 |
<button
|
283 |
class="active-tool base-tool"
|
284 |
disabled={loading}
|
285 |
-
|
|
|
286 |
goto(`${base}/tools/${tool._id}`);
|
287 |
}}
|
288 |
>
|
289 |
-
|
|
|
|
|
290 |
{tool.displayName}
|
291 |
</button>
|
292 |
{/each}
|
@@ -308,22 +345,14 @@
|
|
308 |
{/if}
|
309 |
</div>
|
310 |
{/if}
|
311 |
-
|
312 |
</div>
|
313 |
|
314 |
<style lang="postcss">
|
315 |
-
pre,
|
316 |
-
textarea {
|
317 |
font-family: inherit;
|
318 |
box-sizing: border-box;
|
319 |
line-height: 1.5;
|
320 |
}
|
321 |
-
|
322 |
-
.base-tool {
|
323 |
-
@apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap border border-transparent text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600;
|
324 |
-
}
|
325 |
-
|
326 |
-
.active-tool {
|
327 |
-
@apply rounded-full !border-purple-200 bg-purple-100 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:!border-purple-700 dark:bg-purple-600/40 dark:text-purple-200;
|
328 |
-
}
|
329 |
</style>
|
|
|
23 |
import IconAdd from "~icons/carbon/add";
|
24 |
import { captureScreen } from "$lib/utils/screenshot";
|
25 |
import IconScreenshot from "../icons/IconScreenshot.svelte";
|
26 |
+
import { loginModalOpen } from "$lib/stores/loginModal";
|
27 |
|
28 |
+
interface Props {
|
29 |
+
files?: File[];
|
30 |
+
mimeTypes?: string[];
|
31 |
+
value?: string;
|
32 |
+
placeholder?: string;
|
33 |
+
loading?: boolean;
|
34 |
+
disabled?: boolean;
|
35 |
+
assistant?: Assistant | undefined;
|
36 |
+
modelHasTools?: boolean;
|
37 |
+
modelIsMultimodal?: boolean;
|
38 |
+
children?: import("svelte").Snippet;
|
39 |
+
onPaste?: (e: ClipboardEvent) => void;
|
40 |
+
}
|
41 |
|
42 |
+
let {
|
43 |
+
files = $bindable([]),
|
44 |
+
mimeTypes = [],
|
45 |
+
value = $bindable(""),
|
46 |
+
placeholder = "",
|
47 |
+
loading = false,
|
48 |
+
disabled = false,
|
49 |
+
assistant = undefined,
|
50 |
+
modelHasTools = false,
|
51 |
+
modelIsMultimodal = false,
|
52 |
+
children,
|
53 |
+
onPaste,
|
54 |
+
}: Props = $props();
|
55 |
|
56 |
const onFileChange = async (e: Event) => {
|
57 |
if (!e.target) return;
|
|
|
65 |
}
|
66 |
};
|
67 |
|
68 |
+
let textareaElement: HTMLTextAreaElement | undefined = $state();
|
69 |
+
let isCompositionOn = $state(false);
|
70 |
|
71 |
const dispatch = createEventDispatcher<{ submit: void }>();
|
72 |
|
73 |
onMount(() => {
|
74 |
if (!isVirtualKeyboard()) {
|
75 |
+
textareaElement?.focus();
|
76 |
}
|
77 |
function onFormSubmit() {
|
78 |
adjustTextareaHeight();
|
79 |
}
|
80 |
|
81 |
+
const formEl = textareaElement?.closest("form");
|
82 |
formEl?.addEventListener("submit", onFormSubmit);
|
83 |
return () => {
|
84 |
formEl?.removeEventListener("submit", onFormSubmit);
|
|
|
101 |
}
|
102 |
|
103 |
function adjustTextareaHeight() {
|
104 |
+
if (!textareaElement) {
|
105 |
+
return;
|
106 |
+
}
|
107 |
+
|
108 |
textareaElement.style.height = "auto";
|
109 |
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
|
110 |
|
|
|
130 |
|
131 |
// tool section
|
132 |
|
133 |
+
let webSearchIsOn = $derived(
|
134 |
+
modelHasTools
|
135 |
+
? ($settings.tools?.includes(webSearchToolId) ?? false) ||
|
136 |
+
($settings.tools?.includes(fetchUrlToolId) ?? false)
|
137 |
+
: $webSearchParameters.useSearch
|
138 |
+
);
|
139 |
+
let imageGenIsOn = $derived($settings.tools?.includes(imageGenToolId) ?? false);
|
140 |
|
141 |
+
let documentParserIsOn = $derived(
|
142 |
+
modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"))
|
143 |
+
);
|
144 |
|
145 |
+
let extraTools = $derived(
|
146 |
+
$page.data.tools
|
147 |
+
.filter((t: ToolFront) => $settings.tools?.includes(t._id))
|
148 |
+
.filter(
|
149 |
+
(t: ToolFront) =>
|
150 |
+
![documentParserToolId, imageGenToolId, webSearchToolId, fetchUrlToolId].includes(t._id)
|
151 |
+
) satisfies ToolFront[]
|
152 |
+
);
|
153 |
</script>
|
154 |
|
155 |
+
<div class="flex min-h-full flex-1 flex-col" onpaste={onPaste}>
|
156 |
<textarea
|
157 |
rows="1"
|
158 |
tabindex="0"
|
|
|
161 |
class:text-gray-400={disabled}
|
162 |
bind:value
|
163 |
bind:this={textareaElement}
|
164 |
+
onkeydown={handleKeydown}
|
165 |
+
oncompositionstart={() => (isCompositionOn = true)}
|
166 |
+
oncompositionend={() => (isCompositionOn = false)}
|
167 |
+
oninput={adjustTextareaHeight}
|
168 |
+
onbeforeinput={(ev) => {
|
169 |
+
if ($page.data.loginRequired) {
|
170 |
+
ev.preventDefault();
|
171 |
+
$loginModalOpen = true;
|
172 |
+
}
|
173 |
+
}}
|
174 |
{placeholder}
|
175 |
{disabled}
|
176 |
+
></textarea>
|
177 |
|
178 |
{#if !assistant}
|
179 |
<div
|
|
|
190 |
class="base-tool"
|
191 |
class:active-tool={webSearchIsOn}
|
192 |
disabled={loading}
|
193 |
+
onclick={async (e) => {
|
194 |
+
e.preventDefault();
|
195 |
if (modelHasTools) {
|
196 |
if (webSearchIsOn) {
|
197 |
await settings.instantSet({
|
|
|
227 |
class="base-tool"
|
228 |
class:active-tool={imageGenIsOn}
|
229 |
disabled={loading}
|
230 |
+
onclick={async (e) => {
|
231 |
+
e.preventDefault();
|
232 |
if (modelHasTools) {
|
233 |
if (imageGenIsOn) {
|
234 |
await settings.instantSet({
|
|
|
260 |
return m.split("/")[1];
|
261 |
})
|
262 |
.join(", ")}
|
263 |
+
<div class="flex items-center">
|
264 |
<HoverTooltip
|
265 |
label={mimeTypesString.includes("*")
|
266 |
? "Upload any file"
|
|
|
274 |
class="absolute hidden size-0"
|
275 |
aria-label="Upload file"
|
276 |
type="file"
|
277 |
+
onchange={onFileChange}
|
278 |
accept={mimeTypes.join(",")}
|
279 |
/>
|
280 |
<IconPaperclip classNames="text-xl" />
|
|
|
283 |
{/if}
|
284 |
</label>
|
285 |
</HoverTooltip>
|
286 |
+
</div>
|
287 |
{#if mimeTypes.includes("image/*")}
|
288 |
<HoverTooltip
|
289 |
label="Capture screenshot"
|
|
|
292 |
>
|
293 |
<button
|
294 |
class="base-tool"
|
295 |
+
onclick={async (e) => {
|
296 |
+
e.preventDefault();
|
297 |
const screenshot = await captureScreen();
|
298 |
|
299 |
// Convert base64 to blob
|
|
|
316 |
<button
|
317 |
class="active-tool base-tool"
|
318 |
disabled={loading}
|
319 |
+
onclick={async (e) => {
|
320 |
+
e.preventDefault();
|
321 |
goto(`${base}/tools/${tool._id}`);
|
322 |
}}
|
323 |
>
|
324 |
+
{#key tool.icon + tool.color}
|
325 |
+
<ToolLogo icon={tool.icon} color={tool.color} size="xs" />
|
326 |
+
{/key}
|
327 |
{tool.displayName}
|
328 |
</button>
|
329 |
{/each}
|
|
|
345 |
{/if}
|
346 |
</div>
|
347 |
{/if}
|
348 |
+
{@render children?.()}
|
349 |
</div>
|
350 |
|
351 |
<style lang="postcss">
|
352 |
+
:global(pre),
|
353 |
+
:global(textarea) {
|
354 |
font-family: inherit;
|
355 |
box-sizing: border-box;
|
356 |
line-height: 1.5;
|
357 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
</style>
|
@@ -9,7 +9,11 @@
|
|
9 |
import { base } from "$app/paths";
|
10 |
import JSON5 from "json5";
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
13 |
|
14 |
const announcementBanners = envPublic.PUBLIC_ANNOUNCEMENT_BANNERS
|
15 |
? JSON5.parse(envPublic.PUBLIC_ANNOUNCEMENT_BANNERS)
|
@@ -58,7 +62,9 @@
|
|
58 |
alt=""
|
59 |
/>
|
60 |
{:else}
|
61 |
-
<div
|
|
|
|
|
62 |
{/if}
|
63 |
{currentModel.displayName}
|
64 |
</div>
|
@@ -81,12 +87,12 @@
|
|
81 |
<button
|
82 |
type="button"
|
83 |
class="rounded-xl border bg-gray-50 p-3 text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 max-xl:text-sm xl:p-3.5"
|
84 |
-
|
85 |
>
|
86 |
{example.title}
|
87 |
</button>
|
88 |
{/each}
|
89 |
</div>
|
90 |
</div>{/if}
|
91 |
-
<div class="h-40 sm:h-24"
|
92 |
</div>
|
|
|
9 |
import { base } from "$app/paths";
|
10 |
import JSON5 from "json5";
|
11 |
|
12 |
+
interface Props {
|
13 |
+
currentModel: Model;
|
14 |
+
}
|
15 |
+
|
16 |
+
let { currentModel }: Props = $props();
|
17 |
|
18 |
const announcementBanners = envPublic.PUBLIC_ANNOUNCEMENT_BANNERS
|
19 |
? JSON5.parse(envPublic.PUBLIC_ANNOUNCEMENT_BANNERS)
|
|
|
62 |
alt=""
|
63 |
/>
|
64 |
{:else}
|
65 |
+
<div
|
66 |
+
class="size-4 rounded border border-transparent bg-gray-300 dark:bg-gray-800"
|
67 |
+
></div>
|
68 |
{/if}
|
69 |
{currentModel.displayName}
|
70 |
</div>
|
|
|
87 |
<button
|
88 |
type="button"
|
89 |
class="rounded-xl border bg-gray-50 p-3 text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 max-xl:text-sm xl:p-3.5"
|
90 |
+
onclick={() => dispatch("message", example.prompt)}
|
91 |
>
|
92 |
{example.title}
|
93 |
</button>
|
94 |
{/each}
|
95 |
</div>
|
96 |
</div>{/if}
|
97 |
+
<div class="h-40 sm:h-24"></div>
|
98 |
</div>
|
@@ -1,7 +1,8 @@
|
|
1 |
<script lang="ts">
|
|
|
|
|
2 |
import type { Message } from "$lib/types/Message";
|
3 |
-
import {
|
4 |
-
import { deepestChild } from "$lib/utils/deepestChild";
|
5 |
import { page } from "$app/stores";
|
6 |
|
7 |
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
|
@@ -25,117 +26,106 @@
|
|
25 |
} from "$lib/types/MessageUpdate";
|
26 |
import { base } from "$app/paths";
|
27 |
import ToolUpdate from "./ToolUpdate.svelte";
|
28 |
-
import { useSettingsStore } from "$lib/stores/settings";
|
29 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
30 |
import OpenReasoningResults from "./OpenReasoningResults.svelte";
|
31 |
import Alternatives from "./Alternatives.svelte";
|
32 |
import Vote from "./Vote.svelte";
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
const dispatch = createEventDispatcher<{
|
44 |
retry: { content?: string; id: Message["id"] };
|
45 |
}>();
|
46 |
|
47 |
-
let contentEl: HTMLElement;
|
48 |
-
let
|
49 |
-
let pendingTimeout: ReturnType<typeof setTimeout>;
|
50 |
-
let isCopied = false;
|
51 |
-
|
52 |
-
$: emptyLoad =
|
53 |
-
!message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0));
|
54 |
-
|
55 |
-
const settings = useSettingsStore();
|
56 |
-
|
57 |
-
afterUpdate(() => {
|
58 |
-
if ($settings.disableStream) {
|
59 |
-
return;
|
60 |
-
}
|
61 |
-
|
62 |
-
loadingEl?.$destroy();
|
63 |
-
clearTimeout(pendingTimeout);
|
64 |
-
|
65 |
-
// Add loading animation to the last message if update takes more than 600ms
|
66 |
-
if (isLast && loading && emptyLoad) {
|
67 |
-
pendingTimeout = setTimeout(() => {
|
68 |
-
if (contentEl) {
|
69 |
-
loadingEl = new IconLoading({
|
70 |
-
target: deepestChild(contentEl),
|
71 |
-
props: { classNames: "loading inline ml-2 first:ml-0" },
|
72 |
-
});
|
73 |
-
}
|
74 |
-
}, 600);
|
75 |
-
}
|
76 |
-
});
|
77 |
|
78 |
function handleKeyDown(e: KeyboardEvent) {
|
79 |
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
80 |
-
editFormEl
|
81 |
}
|
82 |
}
|
83 |
|
84 |
-
|
85 |
-
|
86 |
|
87 |
-
|
88 |
-
({ type }) => type === MessageUpdateType.
|
89 |
-
|
|
|
90 |
|
91 |
-
|
92 |
-
({ type }) => type === MessageUpdateType.
|
93 |
-
|
|
|
94 |
|
|
|
|
|
|
|
|
|
|
|
95 |
// filter all updates with type === "tool" then group them by uuid field
|
96 |
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
let webSearchIsDone = true;
|
112 |
-
|
113 |
-
$: webSearchIsDone = searchUpdates.some(
|
114 |
-
(update) => update.subtype === MessageWebSearchUpdateType.Finished
|
115 |
);
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
let editContentEl: HTMLTextAreaElement;
|
130 |
-
let editFormEl: HTMLFormElement;
|
131 |
-
|
132 |
-
$: if (editMode) {
|
133 |
-
tick();
|
134 |
-
if (editContentEl) {
|
135 |
-
editContentEl.value = message.content;
|
136 |
-
editContentEl?.focus();
|
137 |
}
|
138 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
</script>
|
140 |
|
141 |
{#if message.from === "assistant"}
|
@@ -144,8 +134,8 @@
|
|
144 |
data-message-id={message.id}
|
145 |
data-message-role="assistant"
|
146 |
role="presentation"
|
147 |
-
|
148 |
-
|
149 |
>
|
150 |
{#if $page.data?.assistant?.avatar}
|
151 |
<img
|
@@ -199,7 +189,7 @@
|
|
199 |
bind:this={contentEl}
|
200 |
class:mt-2={reasoningUpdates.length > 0 || searchUpdates.length > 0}
|
201 |
>
|
202 |
-
{#if isLast && loading &&
|
203 |
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
204 |
{/if}
|
205 |
|
@@ -269,14 +259,14 @@
|
|
269 |
class="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
270 |
title="Retry"
|
271 |
type="button"
|
272 |
-
|
273 |
dispatch("retry", { id: message.id });
|
274 |
}}
|
275 |
>
|
276 |
<CarbonRotate360 />
|
277 |
</button>
|
278 |
<CopyToClipBoardBtn
|
279 |
-
|
280 |
isCopied = true;
|
281 |
}}
|
282 |
classNames="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
@@ -295,8 +285,8 @@
|
|
295 |
data-message-id={message.id}
|
296 |
data-message-type="user"
|
297 |
role="presentation"
|
298 |
-
|
299 |
-
|
300 |
>
|
301 |
<div class="flex w-full flex-col gap-2">
|
302 |
{#if message.files?.length}
|
@@ -318,8 +308,9 @@
|
|
318 |
<form
|
319 |
class="flex w-full flex-col"
|
320 |
bind:this={editFormEl}
|
321 |
-
|
322 |
-
|
|
|
323 |
editMsdgId = null;
|
324 |
}}
|
325 |
>
|
@@ -328,9 +319,9 @@
|
|
328 |
rows="5"
|
329 |
bind:this={editContentEl}
|
330 |
value={message.content.trim()}
|
331 |
-
|
332 |
required
|
333 |
-
|
334 |
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
335 |
<button
|
336 |
type="submit"
|
@@ -346,7 +337,7 @@
|
|
346 |
<button
|
347 |
type="button"
|
348 |
class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
349 |
-
|
350 |
editMsdgId = null;
|
351 |
}}
|
352 |
>
|
@@ -379,7 +370,7 @@
|
|
379 |
class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
|
380 |
title="Branch"
|
381 |
type="button"
|
382 |
-
|
383 |
>
|
384 |
<CarbonPen />
|
385 |
</button>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { run } from "svelte/legacy";
|
3 |
+
|
4 |
import type { Message } from "$lib/types/Message";
|
5 |
+
import { createEventDispatcher, tick } from "svelte";
|
|
|
6 |
import { page } from "$app/stores";
|
7 |
|
8 |
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
|
|
|
26 |
} from "$lib/types/MessageUpdate";
|
27 |
import { base } from "$app/paths";
|
28 |
import ToolUpdate from "./ToolUpdate.svelte";
|
|
|
29 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
30 |
import OpenReasoningResults from "./OpenReasoningResults.svelte";
|
31 |
import Alternatives from "./Alternatives.svelte";
|
32 |
import Vote from "./Vote.svelte";
|
33 |
|
34 |
+
interface Props {
|
35 |
+
message: Message;
|
36 |
+
loading?: boolean;
|
37 |
+
isAuthor?: boolean;
|
38 |
+
readOnly?: boolean;
|
39 |
+
isTapped?: boolean;
|
40 |
+
alternatives?: Message["id"][];
|
41 |
+
editMsdgId?: Message["id"] | null;
|
42 |
+
isLast?: boolean;
|
43 |
+
}
|
44 |
+
|
45 |
+
let {
|
46 |
+
message,
|
47 |
+
loading = false,
|
48 |
+
isAuthor = true,
|
49 |
+
readOnly = false,
|
50 |
+
isTapped = $bindable(false),
|
51 |
+
alternatives = [],
|
52 |
+
editMsdgId = $bindable(null),
|
53 |
+
isLast = false,
|
54 |
+
}: Props = $props();
|
55 |
|
56 |
const dispatch = createEventDispatcher<{
|
57 |
retry: { content?: string; id: Message["id"] };
|
58 |
}>();
|
59 |
|
60 |
+
let contentEl: HTMLElement | undefined = $state();
|
61 |
+
let isCopied = $state(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
|
63 |
function handleKeyDown(e: KeyboardEvent) {
|
64 |
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
65 |
+
editFormEl?.requestSubmit();
|
66 |
}
|
67 |
}
|
68 |
|
69 |
+
let editContentEl: HTMLTextAreaElement | undefined = $state();
|
70 |
+
let editFormEl: HTMLFormElement | undefined = $state();
|
71 |
|
72 |
+
let searchUpdates = $derived(
|
73 |
+
(message.updates?.filter(({ type }) => type === MessageUpdateType.WebSearch) ??
|
74 |
+
[]) as MessageWebSearchUpdate[]
|
75 |
+
);
|
76 |
|
77 |
+
let reasoningUpdates = $derived(
|
78 |
+
(message.updates?.filter(({ type }) => type === MessageUpdateType.Reasoning) ??
|
79 |
+
[]) as MessageReasoningUpdate[]
|
80 |
+
);
|
81 |
|
82 |
+
let messageFinalAnswer = $derived(
|
83 |
+
message.updates?.find(
|
84 |
+
({ type }) => type === MessageUpdateType.FinalAnswer
|
85 |
+
) as MessageFinalAnswerUpdate
|
86 |
+
);
|
87 |
// filter all updates with type === "tool" then group them by uuid field
|
88 |
|
89 |
+
let toolUpdates = $derived(
|
90 |
+
message.updates
|
91 |
+
?.filter(({ type }) => type === "tool")
|
92 |
+
.reduce(
|
93 |
+
(acc, update) => {
|
94 |
+
if (update.type !== "tool") {
|
95 |
+
return acc;
|
96 |
+
}
|
97 |
+
acc[update.uuid] = acc[update.uuid] ?? [];
|
98 |
+
acc[update.uuid].push(update);
|
99 |
+
return acc;
|
100 |
+
},
|
101 |
+
{} as Record<string, MessageToolUpdate[]>
|
102 |
+
)
|
|
|
|
|
|
|
|
|
103 |
);
|
104 |
+
let urlNotTrailing = $derived($page.url.pathname.replace(/\/$/, ""));
|
105 |
+
let downloadLink = $derived(urlNotTrailing + `/message/${message.id}/prompt`);
|
106 |
+
let webSearchSources = $derived(
|
107 |
+
searchUpdates?.find(
|
108 |
+
(update): update is MessageWebSearchSourcesUpdate =>
|
109 |
+
update.subtype === MessageWebSearchUpdateType.Sources
|
110 |
+
)?.sources
|
111 |
+
);
|
112 |
+
run(() => {
|
113 |
+
if (isCopied) {
|
114 |
+
setTimeout(() => {
|
115 |
+
isCopied = false;
|
116 |
+
}, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
}
|
118 |
+
});
|
119 |
+
let editMode = $derived(editMsdgId === message.id);
|
120 |
+
run(() => {
|
121 |
+
if (editMode) {
|
122 |
+
tick();
|
123 |
+
if (editContentEl) {
|
124 |
+
editContentEl.value = message.content;
|
125 |
+
editContentEl?.focus();
|
126 |
+
}
|
127 |
+
}
|
128 |
+
});
|
129 |
</script>
|
130 |
|
131 |
{#if message.from === "assistant"}
|
|
|
134 |
data-message-id={message.id}
|
135 |
data-message-role="assistant"
|
136 |
role="presentation"
|
137 |
+
onclick={() => (isTapped = !isTapped)}
|
138 |
+
onkeydown={() => (isTapped = !isTapped)}
|
139 |
>
|
140 |
{#if $page.data?.assistant?.avatar}
|
141 |
<img
|
|
|
189 |
bind:this={contentEl}
|
190 |
class:mt-2={reasoningUpdates.length > 0 || searchUpdates.length > 0}
|
191 |
>
|
192 |
+
{#if isLast && loading && message.content.length === 0}
|
193 |
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
194 |
{/if}
|
195 |
|
|
|
259 |
class="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
260 |
title="Retry"
|
261 |
type="button"
|
262 |
+
onclick={() => {
|
263 |
dispatch("retry", { id: message.id });
|
264 |
}}
|
265 |
>
|
266 |
<CarbonRotate360 />
|
267 |
</button>
|
268 |
<CopyToClipBoardBtn
|
269 |
+
onClick={() => {
|
270 |
isCopied = true;
|
271 |
}}
|
272 |
classNames="btn rounded-sm p-1 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
|
|
285 |
data-message-id={message.id}
|
286 |
data-message-type="user"
|
287 |
role="presentation"
|
288 |
+
onclick={() => (isTapped = !isTapped)}
|
289 |
+
onkeydown={() => (isTapped = !isTapped)}
|
290 |
>
|
291 |
<div class="flex w-full flex-col gap-2">
|
292 |
{#if message.files?.length}
|
|
|
308 |
<form
|
309 |
class="flex w-full flex-col"
|
310 |
bind:this={editFormEl}
|
311 |
+
onsubmit={(e) => {
|
312 |
+
e.preventDefault();
|
313 |
+
dispatch("retry", { content: editContentEl?.value, id: message.id });
|
314 |
editMsdgId = null;
|
315 |
}}
|
316 |
>
|
|
|
319 |
rows="5"
|
320 |
bind:this={editContentEl}
|
321 |
value={message.content.trim()}
|
322 |
+
onkeydown={handleKeyDown}
|
323 |
required
|
324 |
+
></textarea>
|
325 |
<div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2">
|
326 |
<button
|
327 |
type="submit"
|
|
|
337 |
<button
|
338 |
type="button"
|
339 |
class="btn rounded-sm p-2 text-sm text-gray-400 hover:text-gray-500 focus:ring-0 dark:text-gray-400 dark:hover:text-gray-300"
|
340 |
+
onclick={() => {
|
341 |
editMsdgId = null;
|
342 |
}}
|
343 |
>
|
|
|
370 |
class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
|
371 |
title="Branch"
|
372 |
type="button"
|
373 |
+
onclick={() => (editMsdgId = message.id)}
|
374 |
>
|
375 |
<CarbonPen />
|
376 |
</button>
|
@@ -1,4 +1,7 @@
|
|
1 |
<script lang="ts">
|
|
|
|
|
|
|
2 |
import type { Message, MessageFile } from "$lib/types/Message";
|
3 |
import { createEventDispatcher, onDestroy, tick } from "svelte";
|
4 |
|
@@ -35,26 +38,43 @@
|
|
35 |
import type { ToolFront } from "$lib/types/Tool";
|
36 |
import { loginModalOpen } from "$lib/stores/loginModal";
|
37 |
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
$: isReadOnly = !models.some((model) => model.id === currentModel.id);
|
51 |
|
52 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
let timeout: ReturnType<typeof setTimeout>;
|
54 |
-
let isSharedRecently = false;
|
55 |
-
let editMsdgId: Message["id"] | null = null;
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
58 |
|
59 |
const dispatch = createEventDispatcher<{
|
60 |
message: string;
|
@@ -72,7 +92,7 @@
|
|
72 |
|
73 |
let lastTarget: EventTarget | null = null;
|
74 |
|
75 |
-
let onDrag = false;
|
76 |
|
77 |
const onDragEnter = (e: DragEvent) => {
|
78 |
lastTarget = e.target;
|
@@ -122,15 +142,23 @@
|
|
122 |
}
|
123 |
};
|
124 |
|
125 |
-
|
126 |
-
|
127 |
lastMessage &&
|
128 |
-
|
129 |
-
|
130 |
-
|
|
|
131 |
|
132 |
-
|
133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
);
|
135 |
|
136 |
function onShare() {
|
@@ -154,47 +182,62 @@
|
|
154 |
}
|
155 |
});
|
156 |
|
157 |
-
let chatContainer: HTMLElement;
|
158 |
|
159 |
async function scrollToBottom() {
|
160 |
await tick();
|
|
|
161 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
162 |
}
|
163 |
|
164 |
// If last message is from user, scroll to bottom
|
165 |
-
|
166 |
-
|
167 |
-
|
|
|
|
|
168 |
|
169 |
const settings = useSettingsStore();
|
170 |
|
171 |
-
|
172 |
-
.
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
189 |
);
|
190 |
-
|
191 |
</script>
|
192 |
|
193 |
<svelte:window
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
198 |
/>
|
199 |
|
200 |
<div class="relative min-h-0 min-w-0">
|
@@ -326,11 +369,11 @@
|
|
326 |
<div class="w-full">
|
327 |
<div class="flex w-full *:mb-3">
|
328 |
{#if loading}
|
329 |
-
<StopGeneratingBtn classNames="ml-auto"
|
330 |
{:else if lastIsError}
|
331 |
<RetryBtn
|
332 |
classNames="ml-auto"
|
333 |
-
|
334 |
if (lastMessage && lastMessage.ancestors) {
|
335 |
dispatch("retry", {
|
336 |
id: lastMessage.id,
|
@@ -341,7 +384,7 @@
|
|
341 |
{:else if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
|
342 |
<div class="ml-auto gap-2">
|
343 |
<ContinueBtn
|
344 |
-
|
345 |
if (lastMessage && lastMessage.ancestors) {
|
346 |
dispatch("continue", {
|
347 |
id: lastMessage?.id,
|
@@ -355,7 +398,10 @@
|
|
355 |
<form
|
356 |
tabindex="-1"
|
357 |
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
|
358 |
-
|
|
|
|
|
|
|
359 |
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 dark:border-gray-600 dark:bg-gray-700
|
360 |
{isReadOnly ? 'opacity-30' : ''}"
|
361 |
>
|
@@ -377,13 +423,7 @@
|
|
377 |
bind:files
|
378 |
mimeTypes={activeMimeTypes}
|
379 |
on:submit={handleSubmit}
|
380 |
-
|
381 |
-
if ($page.data.loginRequired) {
|
382 |
-
ev.preventDefault();
|
383 |
-
$loginModalOpen = true;
|
384 |
-
}
|
385 |
-
}}
|
386 |
-
on:paste={onPaste}
|
387 |
disabled={isReadOnly || lastIsError}
|
388 |
modelHasTools={currentModel.tools}
|
389 |
modelIsMultimodal={currentModel.multimodal}
|
@@ -463,7 +503,7 @@
|
|
463 |
class="flex flex-none items-center hover:text-gray-400 max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
|
464 |
type="button"
|
465 |
class:hover:underline={!isSharedRecently}
|
466 |
-
|
467 |
disabled={isSharedRecently}
|
468 |
>
|
469 |
{#if isSharedRecently}
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { run, createBubbler } from "svelte/legacy";
|
3 |
+
|
4 |
+
const bubble = createBubbler();
|
5 |
import type { Message, MessageFile } from "$lib/types/Message";
|
6 |
import { createEventDispatcher, onDestroy, tick } from "svelte";
|
7 |
|
|
|
38 |
import type { ToolFront } from "$lib/types/Tool";
|
39 |
import { loginModalOpen } from "$lib/stores/loginModal";
|
40 |
|
41 |
+
interface Props {
|
42 |
+
messages?: Message[];
|
43 |
+
messagesAlternatives?: Message["id"][][];
|
44 |
+
loading?: boolean;
|
45 |
+
pending?: boolean;
|
46 |
+
shared?: boolean;
|
47 |
+
currentModel: Model;
|
48 |
+
models: Model[];
|
49 |
+
assistant?: Assistant | undefined;
|
50 |
+
preprompt?: string | undefined;
|
51 |
+
files?: File[];
|
52 |
+
}
|
|
|
53 |
|
54 |
+
let {
|
55 |
+
messages = [],
|
56 |
+
messagesAlternatives = [],
|
57 |
+
loading = false,
|
58 |
+
pending = false,
|
59 |
+
shared = false,
|
60 |
+
currentModel,
|
61 |
+
models,
|
62 |
+
assistant = undefined,
|
63 |
+
preprompt = undefined,
|
64 |
+
files = $bindable([]),
|
65 |
+
}: Props = $props();
|
66 |
+
|
67 |
+
let isReadOnly = $derived(!models.some((model) => model.id === currentModel.id));
|
68 |
+
|
69 |
+
let message: string = $state("");
|
70 |
let timeout: ReturnType<typeof setTimeout>;
|
71 |
+
let isSharedRecently = $state(false);
|
72 |
+
let editMsdgId: Message["id"] | null = $state(null);
|
73 |
+
let pastedLongContent = $state(false);
|
74 |
+
|
75 |
+
run(() => {
|
76 |
+
$page.params.id && (isSharedRecently = false);
|
77 |
+
});
|
78 |
|
79 |
const dispatch = createEventDispatcher<{
|
80 |
message: string;
|
|
|
92 |
|
93 |
let lastTarget: EventTarget | null = null;
|
94 |
|
95 |
+
let onDrag = $state(false);
|
96 |
|
97 |
const onDragEnter = (e: DragEvent) => {
|
98 |
lastTarget = e.target;
|
|
|
142 |
}
|
143 |
};
|
144 |
|
145 |
+
let lastMessage = $derived(browser && (messages.at(-1) as Message));
|
146 |
+
let lastIsError = $derived(
|
147 |
lastMessage &&
|
148 |
+
!loading &&
|
149 |
+
(lastMessage.from === "user" ||
|
150 |
+
lastMessage.updates?.findIndex((u) => u.type === "status" && u.status === "error") !== -1)
|
151 |
+
);
|
152 |
|
153 |
+
let sources = $derived(
|
154 |
+
files?.map<Promise<MessageFile>>((file) =>
|
155 |
+
file2base64(file).then((value) => ({
|
156 |
+
type: "base64",
|
157 |
+
value,
|
158 |
+
mime: file.type,
|
159 |
+
name: file.name,
|
160 |
+
}))
|
161 |
+
)
|
162 |
);
|
163 |
|
164 |
function onShare() {
|
|
|
182 |
}
|
183 |
});
|
184 |
|
185 |
+
let chatContainer: HTMLElement | undefined = $state();
|
186 |
|
187 |
async function scrollToBottom() {
|
188 |
await tick();
|
189 |
+
if (!chatContainer) return;
|
190 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
191 |
}
|
192 |
|
193 |
// If last message is from user, scroll to bottom
|
194 |
+
run(() => {
|
195 |
+
if (lastMessage && lastMessage.from === "user") {
|
196 |
+
scrollToBottom();
|
197 |
+
}
|
198 |
+
});
|
199 |
|
200 |
const settings = useSettingsStore();
|
201 |
|
202 |
+
let mimeTypesFromActiveTools = $derived(
|
203 |
+
$page.data.tools
|
204 |
+
.filter((tool: ToolFront) => {
|
205 |
+
if (assistant) {
|
206 |
+
return assistant.tools?.includes(tool._id);
|
207 |
+
}
|
208 |
+
if (currentModel.tools) {
|
209 |
+
return $settings?.tools?.includes(tool._id) ?? tool.isOnByDefault;
|
210 |
+
}
|
211 |
+
return false;
|
212 |
+
})
|
213 |
+
.flatMap((tool: ToolFront) => tool.mimeTypes ?? [])
|
214 |
+
);
|
215 |
+
|
216 |
+
let activeMimeTypes = $derived(
|
217 |
+
Array.from(
|
218 |
+
new Set([
|
219 |
+
...mimeTypesFromActiveTools, // fetch mime types from active tools either from tool settings or active assistant
|
220 |
+
...(currentModel.tools && !assistant ? ["application/pdf"] : []), // if its a tool model, we can always enable document parser so we always accept pdfs
|
221 |
+
...(currentModel.multimodal
|
222 |
+
? (currentModel.multimodalAcceptedMimetypes ?? ["image/*"])
|
223 |
+
: []), // if its a multimodal model, we always accept images
|
224 |
+
])
|
225 |
+
)
|
226 |
);
|
227 |
+
let isFileUploadEnabled = $derived(activeMimeTypes.length > 0);
|
228 |
</script>
|
229 |
|
230 |
<svelte:window
|
231 |
+
ondragenter={onDragEnter}
|
232 |
+
ondragleave={onDragLeave}
|
233 |
+
ondragover={(e) => {
|
234 |
+
e.preventDefault();
|
235 |
+
bubble("dragover");
|
236 |
+
}}
|
237 |
+
ondrop={(e) => {
|
238 |
+
e.preventDefault();
|
239 |
+
onDrag = false;
|
240 |
+
}}
|
241 |
/>
|
242 |
|
243 |
<div class="relative min-h-0 min-w-0">
|
|
|
369 |
<div class="w-full">
|
370 |
<div class="flex w-full *:mb-3">
|
371 |
{#if loading}
|
372 |
+
<StopGeneratingBtn classNames="ml-auto" onClick={() => dispatch("stop")} />
|
373 |
{:else if lastIsError}
|
374 |
<RetryBtn
|
375 |
classNames="ml-auto"
|
376 |
+
onClick={() => {
|
377 |
if (lastMessage && lastMessage.ancestors) {
|
378 |
dispatch("retry", {
|
379 |
id: lastMessage.id,
|
|
|
384 |
{:else if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
|
385 |
<div class="ml-auto gap-2">
|
386 |
<ContinueBtn
|
387 |
+
onClick={() => {
|
388 |
if (lastMessage && lastMessage.ancestors) {
|
389 |
dispatch("continue", {
|
390 |
id: lastMessage?.id,
|
|
|
398 |
<form
|
399 |
tabindex="-1"
|
400 |
aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
|
401 |
+
onsubmit={(e) => {
|
402 |
+
e.preventDefault();
|
403 |
+
handleSubmit();
|
404 |
+
}}
|
405 |
class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 dark:border-gray-600 dark:bg-gray-700
|
406 |
{isReadOnly ? 'opacity-30' : ''}"
|
407 |
>
|
|
|
423 |
bind:files
|
424 |
mimeTypes={activeMimeTypes}
|
425 |
on:submit={handleSubmit}
|
426 |
+
{onPaste}
|
|
|
|
|
|
|
|
|
|
|
|
|
427 |
disabled={isReadOnly || lastIsError}
|
428 |
modelHasTools={currentModel.tools}
|
429 |
modelIsMultimodal={currentModel.multimodal}
|
|
|
503 |
class="flex flex-none items-center hover:text-gray-400 max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
|
504 |
type="button"
|
505 |
class:hover:underline={!isSharedRecently}
|
506 |
+
onclick={onShare}
|
507 |
disabled={isSharedRecently}
|
508 |
>
|
509 |
{#if isSharedRecently}
|
@@ -1,14 +1,25 @@
|
|
1 |
<script lang="ts">
|
|
|
|
|
|
|
2 |
import { useSettingsStore } from "$lib/stores/settings";
|
3 |
import { documentParserToolId } from "$lib/utils/toolIds";
|
4 |
import CarbonImage from "~icons/carbon/image";
|
5 |
-
// import EosIconsLoading from "~icons/eos-icons/loading";
|
6 |
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
12 |
|
13 |
const settings = useSettingsStore();
|
14 |
|
@@ -74,10 +85,13 @@
|
|
74 |
<div
|
75 |
id="dropzone"
|
76 |
role="form"
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
81 |
class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
|
82 |
? 'border-blue-200 !bg-blue-500/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-500/20 dark:text-blue-500'
|
83 |
: 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { createBubbler } from "svelte/legacy";
|
3 |
+
|
4 |
+
const bubble = createBubbler();
|
5 |
import { useSettingsStore } from "$lib/stores/settings";
|
6 |
import { documentParserToolId } from "$lib/utils/toolIds";
|
7 |
import CarbonImage from "~icons/carbon/image";
|
|
|
8 |
|
9 |
+
interface Props {
|
10 |
+
// import EosIconsLoading from "~icons/eos-icons/loading";
|
11 |
+
files: File[];
|
12 |
+
mimeTypes?: string[];
|
13 |
+
onDrag?: boolean;
|
14 |
+
onDragInner?: boolean;
|
15 |
+
}
|
16 |
|
17 |
+
let {
|
18 |
+
files = $bindable(),
|
19 |
+
mimeTypes = [],
|
20 |
+
onDrag = $bindable(false),
|
21 |
+
onDragInner = $bindable(false),
|
22 |
+
}: Props = $props();
|
23 |
|
24 |
const settings = useSettingsStore();
|
25 |
|
|
|
85 |
<div
|
86 |
id="dropzone"
|
87 |
role="form"
|
88 |
+
ondrop={dropHandle}
|
89 |
+
ondragenter={() => (onDragInner = true)}
|
90 |
+
ondragleave={() => (onDragInner = false)}
|
91 |
+
ondragover={(e) => {
|
92 |
+
e.preventDefault();
|
93 |
+
bubble("dragover");
|
94 |
+
}}
|
95 |
class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
|
96 |
? 'border-blue-200 !bg-blue-500/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-500/20 dark:text-blue-500'
|
97 |
: 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
|
@@ -7,8 +7,12 @@
|
|
7 |
import type { Tokens, TokenizerExtension, RendererExtension } from "marked";
|
8 |
import CodeBlock from "../CodeBlock.svelte";
|
9 |
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
12 |
|
13 |
interface katexBlockToken extends Tokens.Generic {
|
14 |
type: "katexBlock";
|
@@ -135,7 +139,7 @@
|
|
135 |
"&": "&",
|
136 |
"'": "'",
|
137 |
'"': """,
|
138 |
-
}[x] || x
|
139 |
);
|
140 |
}
|
141 |
|
|
|
7 |
import type { Tokens, TokenizerExtension, RendererExtension } from "marked";
|
8 |
import CodeBlock from "../CodeBlock.svelte";
|
9 |
|
10 |
+
interface Props {
|
11 |
+
content: string;
|
12 |
+
sources?: WebSearchSource[];
|
13 |
+
}
|
14 |
+
|
15 |
+
let { content, sources = [] }: Props = $props();
|
16 |
|
17 |
interface katexBlockToken extends Tokens.Generic {
|
18 |
type: "katexBlock";
|
|
|
139 |
"&": "&",
|
140 |
"'": "'",
|
141 |
'"': """,
|
142 |
+
})[x] || x
|
143 |
);
|
144 |
}
|
145 |
|
@@ -4,12 +4,16 @@
|
|
4 |
import { base } from "$app/paths";
|
5 |
import type { Model } from "$lib/types/Model";
|
6 |
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
let selectedModelId =
|
11 |
-
? currentModel.id
|
12 |
-
|
13 |
|
14 |
async function handleModelChange() {
|
15 |
if (!$page.params.id) return;
|
@@ -50,7 +54,7 @@
|
|
50 |
{/each}
|
51 |
</select>
|
52 |
<button
|
53 |
-
|
54 |
disabled={selectedModelId === currentModel.id}
|
55 |
class="rounded-md bg-gray-100 px-2 py-1 dark:bg-gray-900"
|
56 |
>
|
|
|
4 |
import { base } from "$app/paths";
|
5 |
import type { Model } from "$lib/types/Model";
|
6 |
|
7 |
+
interface Props {
|
8 |
+
models: Model[];
|
9 |
+
currentModel: Model;
|
10 |
+
}
|
11 |
+
|
12 |
+
let { models, currentModel }: Props = $props();
|
13 |
|
14 |
+
let selectedModelId = $state(
|
15 |
+
models.map((m) => m.id).includes(currentModel.id) ? currentModel.id : models[0].id
|
16 |
+
);
|
17 |
|
18 |
async function handleModelChange() {
|
19 |
if (!$page.params.id) return;
|
|
|
54 |
{/each}
|
55 |
</select>
|
56 |
<button
|
57 |
+
onclick={handleModelChange}
|
58 |
disabled={selectedModelId === currentModel.id}
|
59 |
class="rounded-md bg-gray-100 px-2 py-1 dark:bg-gray-900"
|
60 |
>
|
@@ -2,9 +2,13 @@
|
|
2 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
3 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
4 |
|
5 |
-
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
8 |
</script>
|
9 |
|
10 |
<details
|
|
|
2 |
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
3 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
4 |
|
5 |
+
interface Props {
|
6 |
+
summary: string;
|
7 |
+
content: string;
|
8 |
+
loading?: boolean;
|
9 |
+
}
|
10 |
+
|
11 |
+
let { summary, content, loading = false }: Props = $props();
|
12 |
</script>
|
13 |
|
14 |
<details
|
@@ -12,35 +12,41 @@
|
|
12 |
import { onDestroy } from "svelte";
|
13 |
import { browser } from "$app/environment";
|
14 |
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
|
18 |
const toolFnName = tool.find(isMessageToolCallUpdate)?.call.name;
|
19 |
-
|
20 |
-
|
21 |
|
22 |
-
|
23 |
|
24 |
const availableTools: ToolFront[] = $page.data.tools;
|
25 |
|
26 |
-
let loadingBarEl: HTMLDivElement;
|
27 |
-
let animation: Animation | undefined = undefined;
|
28 |
-
|
29 |
-
let isShowingLoadingBar = false;
|
30 |
-
|
31 |
-
|
32 |
-
!
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
44 |
|
45 |
onDestroy(() => {
|
46 |
if (animation) {
|
@@ -49,28 +55,30 @@
|
|
49 |
});
|
50 |
|
51 |
// go to 100% quickly if loading is done
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
|
|
|
|
74 |
</script>
|
75 |
|
76 |
{#if toolFnName && toolFnName !== "websearch"}
|
@@ -84,7 +92,7 @@
|
|
84 |
<div
|
85 |
bind:this={loadingBarEl}
|
86 |
class="absolute -m-1 hidden h-full w-[calc(100%+1rem)] rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10"
|
87 |
-
|
88 |
|
89 |
<div
|
90 |
class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
|
@@ -122,7 +130,7 @@
|
|
122 |
{#if toolUpdate.subtype === MessageToolUpdateType.Call}
|
123 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
124 |
<h3 class="text-sm">Parameters</h3>
|
125 |
-
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"
|
126 |
</div>
|
127 |
<ul class="py-1 text-sm">
|
128 |
{#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
|
@@ -137,13 +145,13 @@
|
|
137 |
{:else if toolUpdate.subtype === MessageToolUpdateType.Error}
|
138 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
139 |
<h3 class="text-sm">Error</h3>
|
140 |
-
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"
|
141 |
</div>
|
142 |
<p class="text-sm">{toolUpdate.message}</p>
|
143 |
{:else if isMessageToolResultUpdate(toolUpdate) && toolUpdate.result.status === ToolResultStatus.Success && toolUpdate.result.display}
|
144 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
145 |
<h3 class="text-sm">Result</h3>
|
146 |
-
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"
|
147 |
</div>
|
148 |
<ul class="py-1 text-sm">
|
149 |
{#each toolUpdate.result.outputs as output}
|
|
|
12 |
import { onDestroy } from "svelte";
|
13 |
import { browser } from "$app/environment";
|
14 |
|
15 |
+
interface Props {
|
16 |
+
tool: MessageToolUpdate[];
|
17 |
+
loading?: boolean;
|
18 |
+
}
|
19 |
+
|
20 |
+
let { tool, loading = false }: Props = $props();
|
21 |
|
22 |
const toolFnName = tool.find(isMessageToolCallUpdate)?.call.name;
|
23 |
+
let toolError = $derived(tool.some(isMessageToolErrorUpdate));
|
24 |
+
let toolDone = $derived(tool.some(isMessageToolResultUpdate));
|
25 |
|
26 |
+
let eta = $derived(tool.find((el) => el.subtype === MessageToolUpdateType.ETA)?.eta);
|
27 |
|
28 |
const availableTools: ToolFront[] = $page.data.tools;
|
29 |
|
30 |
+
let loadingBarEl: HTMLDivElement | undefined = $state();
|
31 |
+
let animation: Animation | undefined = $state(undefined);
|
32 |
+
|
33 |
+
let isShowingLoadingBar = $state(false);
|
34 |
+
|
35 |
+
$effect(() => {
|
36 |
+
!toolError &&
|
37 |
+
!toolDone &&
|
38 |
+
loading &&
|
39 |
+
loadingBarEl &&
|
40 |
+
eta &&
|
41 |
+
(() => {
|
42 |
+
loadingBarEl.classList.remove("hidden");
|
43 |
+
isShowingLoadingBar = true;
|
44 |
+
animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
|
45 |
+
duration: eta * 1000,
|
46 |
+
fill: "forwards",
|
47 |
+
});
|
48 |
+
})();
|
49 |
+
});
|
50 |
|
51 |
onDestroy(() => {
|
52 |
if (animation) {
|
|
|
55 |
});
|
56 |
|
57 |
// go to 100% quickly if loading is done
|
58 |
+
$effect(() => {
|
59 |
+
(!loading || toolDone || toolError) &&
|
60 |
+
browser &&
|
61 |
+
loadingBarEl &&
|
62 |
+
isShowingLoadingBar &&
|
63 |
+
(() => {
|
64 |
+
isShowingLoadingBar = false;
|
65 |
+
|
66 |
+
loadingBarEl.classList.remove("hidden");
|
67 |
+
|
68 |
+
animation?.cancel();
|
69 |
+
animation = loadingBarEl.animate(
|
70 |
+
[{ width: loadingBarEl.style.width }, { width: "calc(100%+1rem)" }],
|
71 |
+
{
|
72 |
+
duration: 300,
|
73 |
+
fill: "forwards",
|
74 |
+
}
|
75 |
+
);
|
76 |
+
|
77 |
+
setTimeout(() => {
|
78 |
+
loadingBarEl?.classList.add("hidden");
|
79 |
+
}, 300);
|
80 |
+
})();
|
81 |
+
});
|
82 |
</script>
|
83 |
|
84 |
{#if toolFnName && toolFnName !== "websearch"}
|
|
|
92 |
<div
|
93 |
bind:this={loadingBarEl}
|
94 |
class="absolute -m-1 hidden h-full w-[calc(100%+1rem)] rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10"
|
95 |
+
></div>
|
96 |
|
97 |
<div
|
98 |
class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
|
|
|
130 |
{#if toolUpdate.subtype === MessageToolUpdateType.Call}
|
131 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
132 |
<h3 class="text-sm">Parameters</h3>
|
133 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
134 |
</div>
|
135 |
<ul class="py-1 text-sm">
|
136 |
{#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
|
|
|
145 |
{:else if toolUpdate.subtype === MessageToolUpdateType.Error}
|
146 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
147 |
<h3 class="text-sm">Error</h3>
|
148 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
149 |
</div>
|
150 |
<p class="text-sm">{toolUpdate.message}</p>
|
151 |
{:else if isMessageToolResultUpdate(toolUpdate) && toolUpdate.result.status === ToolResultStatus.Success && toolUpdate.result.display}
|
152 |
<div class="mt-1 flex items-center gap-2 opacity-80">
|
153 |
<h3 class="text-sm">Result</h3>
|
154 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
155 |
</div>
|
156 |
<ul class="py-1 text-sm">
|
157 |
{#each toolUpdate.result.outputs as output}
|
@@ -11,11 +11,16 @@
|
|
11 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
12 |
import { base } from "$app/paths";
|
13 |
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
16 |
|
17 |
-
|
18 |
-
|
|
|
19 |
|
20 |
const dispatch = createEventDispatcher<{ close: void }>();
|
21 |
|
@@ -47,7 +52,7 @@
|
|
47 |
mime === "application/xml" ||
|
48 |
mime === "application/vnd.chatui.clipboard";
|
49 |
|
50 |
-
|
51 |
</script>
|
52 |
|
53 |
{#if showModal && isClickable}
|
@@ -80,7 +85,7 @@
|
|
80 |
{/if}
|
81 |
<button
|
82 |
class="absolute right-4 top-4 text-xl text-gray-500 hover:text-gray-800"
|
83 |
-
|
84 |
>
|
85 |
<CarbonClose class="text-xl" />
|
86 |
</button>
|
@@ -110,7 +115,20 @@
|
|
110 |
</Modal>
|
111 |
{/if}
|
112 |
|
113 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
<div class="group relative flex items-center rounded-xl shadow-sm">
|
115 |
{#if isImage(file.mime)}
|
116 |
<div class="size-48 overflow-hidden rounded-xl">
|
@@ -133,18 +151,18 @@
|
|
133 |
<div
|
134 |
class="border-1 w-72 overflow-clip rounded-xl border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
|
135 |
>
|
136 |
-
<!-- svelte-ignore
|
137 |
<video
|
138 |
src={file.type === "base64"
|
139 |
? `data:${file.mime};base64,${file.value}`
|
140 |
: urlNotTrailing + "/output/" + file.value}
|
141 |
controls
|
142 |
-
|
143 |
</div>
|
144 |
{:else if isPlainText(file.mime)}
|
145 |
<div
|
146 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
147 |
-
class:hoverable={isClickable}
|
148 |
>
|
149 |
<div
|
150 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
@@ -165,7 +183,7 @@
|
|
165 |
{:else if file.mime === "octet-stream"}
|
166 |
<div
|
167 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
168 |
-
class:hoverable={isClickable}
|
169 |
>
|
170 |
<div
|
171 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
@@ -191,7 +209,7 @@
|
|
191 |
{:else}
|
192 |
<div
|
193 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
194 |
-
class:hoverable={isClickable}
|
195 |
>
|
196 |
<div
|
197 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
@@ -211,16 +229,14 @@
|
|
211 |
<button
|
212 |
class="absolute -right-2 -top-2 z-10 grid size-6 place-items-center rounded-full border bg-black group-hover:visible dark:border-gray-700"
|
213 |
class:invisible={navigator.maxTouchPoints === 0}
|
214 |
-
|
|
|
|
|
|
|
|
|
215 |
>
|
216 |
<CarbonClose class=" text-xs text-white" />
|
217 |
</button>
|
218 |
{/if}
|
219 |
</div>
|
220 |
-
</
|
221 |
-
|
222 |
-
<style lang="postcss">
|
223 |
-
.hoverable {
|
224 |
-
@apply hover:bg-gray-500/10;
|
225 |
-
}
|
226 |
-
</style>
|
|
|
11 |
import EosIconsLoading from "~icons/eos-icons/loading";
|
12 |
import { base } from "$app/paths";
|
13 |
|
14 |
+
interface Props {
|
15 |
+
file: MessageFile;
|
16 |
+
canClose?: boolean;
|
17 |
+
}
|
18 |
+
|
19 |
+
let { file, canClose = true }: Props = $props();
|
20 |
|
21 |
+
let showModal = $state(false);
|
22 |
+
|
23 |
+
let urlNotTrailing = $derived($page.url.pathname.replace(/\/$/, ""));
|
24 |
|
25 |
const dispatch = createEventDispatcher<{ close: void }>();
|
26 |
|
|
|
52 |
mime === "application/xml" ||
|
53 |
mime === "application/vnd.chatui.clipboard";
|
54 |
|
55 |
+
let isClickable = $derived(isImage(file.mime) || isPlainText(file.mime));
|
56 |
</script>
|
57 |
|
58 |
{#if showModal && isClickable}
|
|
|
85 |
{/if}
|
86 |
<button
|
87 |
class="absolute right-4 top-4 text-xl text-gray-500 hover:text-gray-800"
|
88 |
+
onclick={() => (showModal = false)}
|
89 |
>
|
90 |
<CarbonClose class="text-xl" />
|
91 |
</button>
|
|
|
115 |
</Modal>
|
116 |
{/if}
|
117 |
|
118 |
+
<div
|
119 |
+
onclick={() => isClickable && (showModal = true)}
|
120 |
+
onkeydown={(e) => {
|
121 |
+
if (!isClickable) {
|
122 |
+
return;
|
123 |
+
}
|
124 |
+
if (e.key === "Enter" || e.key === " ") {
|
125 |
+
showModal = true;
|
126 |
+
}
|
127 |
+
}}
|
128 |
+
class:clickable={isClickable}
|
129 |
+
role="button"
|
130 |
+
tabindex="0"
|
131 |
+
>
|
132 |
<div class="group relative flex items-center rounded-xl shadow-sm">
|
133 |
{#if isImage(file.mime)}
|
134 |
<div class="size-48 overflow-hidden rounded-xl">
|
|
|
151 |
<div
|
152 |
class="border-1 w-72 overflow-clip rounded-xl border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
|
153 |
>
|
154 |
+
<!-- svelte-ignore a11y_media_has_caption -->
|
155 |
<video
|
156 |
src={file.type === "base64"
|
157 |
? `data:${file.mime};base64,${file.value}`
|
158 |
: urlNotTrailing + "/output/" + file.value}
|
159 |
controls
|
160 |
+
></video>
|
161 |
</div>
|
162 |
{:else if isPlainText(file.mime)}
|
163 |
<div
|
164 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
165 |
+
class:file-hoverable={isClickable}
|
166 |
>
|
167 |
<div
|
168 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
|
183 |
{:else if file.mime === "octet-stream"}
|
184 |
<div
|
185 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
186 |
+
class:file-hoverable={isClickable}
|
187 |
>
|
188 |
<div
|
189 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
|
209 |
{:else}
|
210 |
<div
|
211 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
212 |
+
class:file-hoverable={isClickable}
|
213 |
>
|
214 |
<div
|
215 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
|
229 |
<button
|
230 |
class="absolute -right-2 -top-2 z-10 grid size-6 place-items-center rounded-full border bg-black group-hover:visible dark:border-gray-700"
|
231 |
class:invisible={navigator.maxTouchPoints === 0}
|
232 |
+
onclick={(e) => {
|
233 |
+
e.preventDefault();
|
234 |
+
e.stopPropagation();
|
235 |
+
dispatch("close");
|
236 |
+
}}
|
237 |
>
|
238 |
<CarbonClose class=" text-xs text-white" />
|
239 |
</button>
|
240 |
{/if}
|
241 |
</div>
|
242 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|