Skip to content

Commit

Permalink
Merge branch 'main' into add-speech-recognition
Browse files Browse the repository at this point in the history
  • Loading branch information
nsarrazin authored Jan 16, 2025
2 parents a63bb83 + 795bf39 commit 9a1cec9
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 126 deletions.
2 changes: 1 addition & 1 deletion chart/env/prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ envVars:
PUBLIC_APP_COLOR: "yellow"
PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone."
PUBLIC_APP_DISCLAIMER_MESSAGE: "Disclaimer: AI is an area of active research with known problems such as biased generation and misinformation. Do not use this application for high-stakes decisions or advice."
PUBLIC_APP_GUEST_MESSAGE: "You have reached the guest message limit, Sign In with a free Hugging Face account to continue using HuggingChat."
PUBLIC_APP_GUEST_MESSAGE: "Sign in with a free Hugging Face account to continue using HuggingChat."
PUBLIC_APP_DATA_SHARING: 0
PUBLIC_APP_DISCLAIMER: 1
PUBLIC_PLAUSIBLE_SCRIPT_URL: "/js/script.js"
Expand Down
46 changes: 46 additions & 0 deletions src/lib/components/InfiniteScroll.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
let loader: HTMLDivElement;
let observer: IntersectionObserver;
let intervalId: ReturnType<typeof setInterval> | undefined;
onMount(() => {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Clear any existing interval
if (intervalId) {
clearInterval(intervalId);
}
// Start new interval that dispatches every 250ms
intervalId = setInterval(() => {
dispatch("visible");
}, 250);
} else {
// Clear interval when not intersecting
if (intervalId) {
clearInterval(intervalId);
intervalId = undefined;
}
}
});
});
observer.observe(loader);
return () => {
observer.disconnect();
if (intervalId) {
clearInterval(intervalId);
}
};
});
</script>

<div bind:this={loader} class="flex animate-pulse flex-col gap-4">
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
<div class="ml-2 h-5 w-4/5 gap-5 rounded bg-gray-200 dark:bg-gray-700" />
</div>
37 changes: 37 additions & 0 deletions src/lib/components/NavMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
import type { ConvSidebar } from "$lib/types/ConvSidebar";
import type { Model } from "$lib/types/Model";
import { page } from "$app/stores";
import InfiniteScroll from "./InfiniteScroll.svelte";
import type { Conversation } from "$lib/types/Conversation";
import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
export let conversations: ConvSidebar[];
export let canLogin: boolean;
export let user: LayoutData["user"];
export let p = 0;
let hasMore = true;
function handleNewChatClick() {
isAborted.set(true);
}
Expand Down Expand Up @@ -44,6 +51,33 @@
} as const;
const nModels: number = $page.data.models.filter((el: Model) => !el.unlisted).length;
async function handleVisible() {
p++;
const newConvs = await fetch(`${base}/api/conversations?p=${p}`)
.then((res) => res.json())
.then((convs) =>
convs.map(
(conv: Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">) => ({
...conv,
updatedAt: new Date(conv.updatedAt),
})
)
)
.catch(() => []);
if (newConvs.length === 0) {
hasMore = false;
}
conversations = [...conversations, ...newConvs];
}
$: if (conversations.length <= CONV_NUM_PER_PAGE) {
// reset p to 0 if there's only one page of content
// that would be caused by a data loading invalidation
p = 0;
}
</script>

<div class="sticky top-0 flex flex-none items-center justify-between px-1.5 py-3.5 max-sm:pt-0">
Expand Down Expand Up @@ -89,6 +123,9 @@
{/if}
{/each}
</div>
{#if hasMore}
<InfiniteScroll on:visible={handleVisible} />
{/if}
{/await}
</div>
<div
Expand Down
105 changes: 52 additions & 53 deletions src/lib/components/chat/ChatInput.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { browser } from "$app/environment";
import { createEventDispatcher, onMount, tick } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
import HoverTooltip from "$lib/components/HoverTooltip.svelte";
import IconInternet from "$lib/components/icons/IconInternet.svelte";
Expand Down Expand Up @@ -31,7 +31,6 @@
export let placeholder = "";
export let loading = false;
export let disabled = false;
export let assistant: Assistant | undefined = undefined;
export let modelHasTools = false;
Expand All @@ -54,6 +53,21 @@
const dispatch = createEventDispatcher<{ submit: void }>();
onMount(() => {
if (!isVirtualKeyboard()) {
textareaElement.focus();
}
function onFormSubmit() {
adjustTextareaHeight();
}
const formEl = textareaElement.closest("form");
formEl?.addEventListener("submit", onFormSubmit);
return () => {
formEl?.removeEventListener("submit", onFormSubmit);
};
});
function isVirtualKeyboard(): boolean {
if (!browser) return false;
Expand All @@ -70,30 +84,24 @@
}
function adjustTextareaHeight() {
if (!textareaElement) return;
textareaElement.style.height = "auto";
const newHeight = Math.min(textareaElement.scrollHeight, parseInt("96em"));
textareaElement.style.height = `${newHeight}px`;
if (!textareaElement.parentElement) return;
textareaElement.parentElement.style.height = `${newHeight}px`;
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
if (textareaElement.selectionStart === textareaElement.value.length) {
textareaElement.scrollTop = textareaElement.scrollHeight;
}
}
async function handleKeydown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
function handleKeydown(event: KeyboardEvent) {
if (
event.key === "Enter" &&
!event.shiftKey &&
!isCompositionOn &&
!isVirtualKeyboard() &&
value.trim() !== ""
) {
event.preventDefault();
if (isVirtualKeyboard()) {
// Insert a newline at the cursor position
const start = textareaElement.selectionStart;
const end = textareaElement.selectionEnd;
value = value.substring(0, start) + "\n" + value.substring(end);
textareaElement.selectionStart = textareaElement.selectionEnd = start + 1;
} else {
if (value.trim() !== "") {
dispatch("submit");
await tick();
adjustTextareaHeight();
}
}
dispatch("submit");
}
}
Expand All @@ -110,13 +118,6 @@
$: documentParserIsOn =
modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"));
onMount(() => {
if (!isVirtualKeyboard()) {
textareaElement.focus();
}
adjustTextareaHeight();
});
$: extraTools = $page.data.tools
.filter((t: ToolFront) => $settings.tools?.includes(t._id))
.filter(
Expand All @@ -125,29 +126,27 @@
) satisfies ToolFront[];
</script>

<div class="min-h-full flex-1" on:paste>
<div class="relative w-full min-w-0">
<textarea
enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
tabindex="0"
rows="1"
class="scrollbar-custom max-h-[96em] w-full resize-none scroll-p-3 overflow-y-auto overflow-x-hidden border-0 bg-transparent px-3 py-2.5 outline-none focus:ring-0 focus-visible:ring-0 max-sm:p-2.5 max-sm:text-[16px]"
class:text-gray-400={disabled}
bind:value
bind:this={textareaElement}
{disabled}
on:keydown={handleKeydown}
on:compositionstart={() => (isCompositionOn = true)}
on:compositionend={() => (isCompositionOn = false)}
on:input={adjustTextareaHeight}
on:beforeinput
{placeholder}
/>
</div>
<div class="flex min-h-full flex-1 flex-col" on:paste>
<textarea
rows="1"
tabindex="0"
inputmode="text"
class="scrollbar-custom max-h-[4lh] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent px-2.5 py-2.5 outline-none focus:ring-0 focus-visible:ring-0 max-sm:text-[16px] sm:px-3"
class:text-gray-400={disabled}
bind:value
bind:this={textareaElement}
on:keydown={handleKeydown}
on:compositionstart={() => (isCompositionOn = true)}
on:compositionend={() => (isCompositionOn = false)}
on:input={adjustTextareaHeight}
on:beforeinput
{placeholder}
{disabled}
/>

{#if !assistant}
<div
class="scrollbar-custom -ml-0.5 flex max-w-[calc(100%-40px)] flex-wrap items-center justify-start gap-2 px-3 pb-2.5 pt-0.5 text-gray-500
dark:text-gray-400 max-md:flex-nowrap max-md:overflow-x-auto sm:gap-2.5"
class="scrollbar-custom -ml-0.5 flex max-w-[calc(100%-40px)] flex-wrap items-center justify-start gap-2.5 px-3 pb-2.5 pt-1.5 text-gray-500 dark:text-gray-400 max-md:flex-nowrap max-md:overflow-x-auto sm:gap-2"
>
<HoverTooltip
label="Search the web"
Expand Down Expand Up @@ -299,7 +298,7 @@
TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 max-sm:hidden"
>
<a
class="base-tool flex !size-[20px] items-center justify-center rounded-full bg-white/10"
class="base-tool flex !size-[20px] items-center justify-center rounded-full border !border-gray-200 !bg-white !transition-none dark:!border-gray-500 dark:!bg-transparent"
href={`${base}/tools`}
title="Browse more tools"
>
Expand All @@ -321,10 +320,10 @@
}
.base-tool {
@apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600;
@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;
}
.active-tool {
@apply rounded-full bg-purple-500/15 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:bg-purple-600/40 dark:text-purple-300;
@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;
}
</style>
Loading

0 comments on commit 9a1cec9

Please sign in to comment.