Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/lib/components/CopyToClipBoardBtn.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onDestroy } from "svelte";

import CarbonCopy from "~icons/carbon/copy";
import CarbonCheckmark from "~icons/carbon/checkmark";
import Tooltip from "./Tooltip.svelte";

interface Props {
Expand Down Expand Up @@ -67,6 +68,8 @@
clearTimeout(timeout);
}
});

const Icon = $derived(isSuccess ? CarbonCheckmark : CarbonCopy);
</script>

<button
Expand All @@ -79,8 +82,10 @@
}}
>
<div class="relative">
{#if children}{@render children()}{:else}
<CarbonCopy class={iconClassNames} />
{#if children}
{@render children()}
{:else}
<Icon class={iconClassNames} />
{/if}

{#if showTooltip}
Expand Down
58 changes: 16 additions & 42 deletions src/lib/components/chat/ChatMessage.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
<script lang="ts">
import type { Message } from "$lib/types/Message";
import { tick } from "svelte";

import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
const publicConfig = usePublicConfig();
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
import IconLoading from "../icons/IconLoading.svelte";
import CarbonRotate360 from "~icons/carbon/rotate-360";
// import CarbonDownload from "~icons/carbon/download";

import CarbonPen from "~icons/carbon/pen";
import UploadedFile from "./UploadedFile.svelte";

import {
MessageReasoningUpdateType,
MessageUpdateType,
type MessageReasoningUpdate,
MessageReasoningUpdateType,
} from "$lib/types/MessageUpdate";
import MarkdownRenderer from "./MarkdownRenderer.svelte";
import OpenReasoningResults from "./OpenReasoningResults.svelte";
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
import CarbonPen from "~icons/carbon/pen";
import CarbonRotate360 from "~icons/carbon/rotate-360";
import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte";
import IconLoading from "../icons/IconLoading.svelte";
import Alternatives from "./Alternatives.svelte";
import MarkdownRenderer from "./MarkdownRenderer.svelte";
import MessageAvatar from "./MessageAvatar.svelte";
import OpenReasoningResults from "./OpenReasoningResults.svelte";
import UploadedFile from "./UploadedFile.svelte";

interface Props {
message: Message;
Expand Down Expand Up @@ -48,8 +42,9 @@
onshowAlternateMsg,
}: Props = $props();

const publicConfig = usePublicConfig();

let contentEl: HTMLElement | undefined = $state();
let isCopied = $state(false);
let messageWidth: number = $state(0);
let messageInfoWidth: number = $state(0);

Expand All @@ -68,7 +63,6 @@
}
}

let editContentEl: HTMLTextAreaElement | undefined = $state();
let editFormEl: HTMLFormElement | undefined = $state();

let reasoningUpdates = $derived(
Expand All @@ -95,24 +89,8 @@
);
let hasClientThink = $derived(!hasServerReasoning && thinkSegments.length > 1);

$effect(() => {
if (isCopied) {
setTimeout(() => {
isCopied = false;
}, 1000);
}
});

let editMode = $derived(editMsdgId === message.id);
$effect(() => {
if (editMode) {
tick();
if (editContentEl) {
editContentEl.value = message.content;
editContentEl?.focus();
}
}
});
let editedContent = $derived(message.content);
</script>

{#if message.from === "assistant"}
Expand All @@ -129,7 +107,7 @@
onkeydown={() => (isTapped = !isTapped)}
>
<MessageAvatar
classNames="mt-5 size-3.5 flex-none select-none rounded-full shadow-lg max-sm:hidden"
classNames="sticky top-4 mb-5 mt-5 size-5 flex-none select-none rounded-full shadow-lg max-sm:hidden"
animating={isLast && loading}
/>
<div
Expand Down Expand Up @@ -223,9 +201,6 @@
{/if}
{#if !isLast || !loading}
<CopyToClipBoardBtn
onClick={() => {
isCopied = true;
}}
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"
value={message.content}
iconClassNames="text-xs"
Expand Down Expand Up @@ -286,15 +261,14 @@
bind:this={editFormEl}
onsubmit={(e) => {
e.preventDefault();
onretry?.({ content: editContentEl?.value, id: message.id });
onretry?.({ content: editedContent, id: message.id });
editMsdgId = null;
}}
>
<textarea
class="w-full whitespace-break-spaces break-words rounded-xl bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max focus:outline-none dark:bg-gray-800 dark:text-gray-400"
rows="5"
bind:this={editContentEl}
value={message.content.trim()}
bind:value={editedContent}
onkeydown={handleKeyDown}
required
></textarea>
Expand Down Expand Up @@ -334,7 +308,7 @@
{/if}
{#if (alternatives.length > 1 && editMsdgId === null) || (!loading && !editMode)}
<button
class="hidden cursor-pointer items-center gap-1 rounded-md border border-gray-200 px-1.5 py-0.5 text-xs text-gray-400 group-hover:flex hover:flex hover:text-gray-500 dark:border-gray-700 dark:text-gray-400 dark:hover:text-gray-300 lg:-right-2"
class="flex cursor-pointer items-center gap-1 rounded-md border border-gray-200 px-1.5 py-0.5 text-xs text-gray-400 opacity-0 group-hover:opacity-100 hover:text-gray-500 hover:opacity-100 dark:border-gray-700 dark:text-gray-400 dark:hover:text-gray-300 lg:-right-2"
title="Edit"
type="button"
onclick={() => (editMsdgId = message.id)}
Expand Down
62 changes: 20 additions & 42 deletions src/lib/components/chat/MessageAvatar.svelte
Original file line number Diff line number Diff line change
@@ -1,46 +1,32 @@
<script lang="ts">
import { onDestroy } from "svelte";
const { animating = false, classNames = "" } = $props();

let { animating = false, classNames = "" } = $props();

let blobAnim: SVGAnimateElement | undefined = $state();
let svgEl: SVGSVGElement | undefined = $state();

// Only trigger begin/end on transitions, and pause when not animating
let prevAnimating: boolean | undefined = undefined;
let prevBlobAnim: SVGAnimateElement | undefined = undefined;
let blobAnim = $state<SVGAnimateElement>();
let svgEl = $state<SVGSVGElement>();
let begun = $state(false);

$effect(() => {
if (!blobAnim) return;
const blobChanged = blobAnim !== prevBlobAnim;
const animChanged = animating !== prevAnimating;
if (!(blobChanged || animChanged)) return;

if (animating) {
// Resume animations and start once
if (!begun) {
blobAnim.beginElement();
begun = true;
}
svgEl?.unpauseAnimations?.();
blobAnim.beginElement();
} else {
// Stop current run and pause so it cannot restart from queued begins
blobAnim.endElement();
svgEl?.pauseAnimations?.();
}
prevAnimating = animating;
prevBlobAnim = blobAnim;
});

onDestroy(() => {
blobAnim?.endElement();
svgEl?.pauseAnimations?.();
return () => {
svgEl?.pauseAnimations?.();
};
});
</script>

<svg
bind:this={svgEl}
class={classNames}
id="ball"
width="1em"
height="1em"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -54,18 +40,23 @@
</mask>

<!-- the blurred black shape inside the circular mask -->
<g filter="url(#c)" mask="url(#b)">
<g mask="url(#b)">
<!-- BASE state (normalized to absolute L commands) -->
<path id="blob" fill="#000" d="M11 1 L8 -4 L3 -8 L-6 6 L3 12 L7 11 L6 2 L11 1 Z">
<path
class="blur-[1.2px]"
id="blob"
fill="#000"
d="M11 1 L8 -4 L3 -8 L-6 6 L3 12 L7 11 L6 2 L11 1 Z"
>
<!-- MORPH: base -> mid -> far -> mid -> base -->
<animate
bind:this={blobAnim}
attributeName="d"
begin="indefinite"
end="indefinite"
dur="3.2s"
repeatCount="indefinite"
fill="remove"
repeatCount={"indefinite"}
fill="freeze"
calcMode="spline"
keyTimes="0; .33; .66; .9; 1"
keySplines="
Expand All @@ -86,18 +77,5 @@

<defs>
<clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z" /></clipPath>
<filter
id="c"
x="-9.4"
y="-10.8"
width="23.8"
height="26"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="1.6" />
</filter>
</defs>
</svg>
5 changes: 5 additions & 0 deletions src/lib/utils/messageUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type MessageUpdateRequestOptions = {
isRetry: boolean;
isContinue: boolean;
files?: MessageFile[];
createdMessageIds?: {
userMessageId?: string;
assistantMessageId?: string;
};
};
export async function fetchMessageUpdates(
conversationId: string,
Expand All @@ -30,6 +34,7 @@ export async function fetchMessageUpdates(
id: opts.messageId,
is_retry: opts.isRetry,
is_continue: opts.isContinue,
created_message_ids: opts.createdMessageIds,
});

opts.files?.forEach((file) => {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/utils/tree/addChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Tree, TreeId, NewNode, TreeNode } from "./tree";
export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: TreeId): TreeId {
// if this is the first message we just push it
if (conv.messages.length === 0) {
const messageId = v4();
const messageId = "id" in message && message.id ? message.id : v4();
conv.rootMessageId = messageId;
conv.messages.push({
...message,
Expand All @@ -18,7 +18,7 @@ export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: Tr
throw new Error("You need to specify a parentId if this is not the first message");
}

const messageId = v4();
const messageId = "id" in message && message.id ? message.id : v4();
if (!conv.rootMessageId) {
// if there is no parentId we just push the message
if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/tree/addSibling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function addSibling<T>(conv: Tree<T>, message: NewNode<T>, siblingId: Tre
throw new Error("The sibling message is the root message, therefore we can't add a sibling");
}

const messageId = v4();
const messageId = "id" in message && message.id ? message.id : v4();

conv.messages.push({
...message,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/tree/tree.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export type TreeNode<T> = T & {
children?: TreeId[];
};

export type NewNode<T> = Omit<TreeNode<T>, "id">;
export type NewNode<T> = Omit<TreeNode<T>, "id"> | TreeNode<T>;
Loading