Skip to content

Commit b19cc4b

Browse files
authored
Merge branch 'main' into embed_assistant
2 parents 8749982 + d60cfa3 commit b19cc4b

File tree

20 files changed

+203
-44
lines changed

20 files changed

+203
-44
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead
132132

133133
RATE_LIMIT= # /!\ Legacy definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
134134
MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
135+
PUBLIC_APP_GUEST_MESSAGE=# a message to the guest user. If not set, a default message will be used
135136

136137
APP_BASE="" # base path of the app, e.g. /chat, left blank as default
137138
PUBLIC_APP_NAME=ChatUI # name used as title throughout the app

chart/env/prod.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ envVars:
285285
PUBLIC_APP_COLOR: "yellow"
286286
PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone."
287287
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."
288+
PUBLIC_APP_GUEST_MESSAGE: "You have reached the guest message limit, Sign In with a free Hugging Face account to continue using HuggingChat."
288289
PUBLIC_APP_DATA_SHARING: 0
289290
PUBLIC_APP_DISCLAIMER: 1
290291
PUBLIC_PLAUSIBLE_SCRIPT_URL: "/js/script.js"

docs/source/configuration/models/providers/google.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ MODELS=`[
3333
"type": "vertex",
3434
"project": "abc-xyz",
3535
"location": "europe-west3",
36-
"model": "gemini-1.5-pro-preview-0409", // model-name
37-
36+
"extraBody": {
37+
"model_version": "gemini-1.5-pro-002",
38+
},
3839
// Optional
3940
"safetyThreshold": "BLOCK_MEDIUM_AND_ABOVE",
4041
"apiEndpoint": "", // alternative api endpoint url,

src/lib/components/LoginModal.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
{envPublic.PUBLIC_APP_DESCRIPTION}
2424
</p>
2525
<p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
26-
You have reached the guest message limit, <strong class="font-semibold"
27-
>Sign In with a free Hugging Face account</strong
28-
> to continue using HuggingChat.
26+
{envPublic.PUBLIC_APP_GUEST_MESSAGE}
2927
</p>
3028

3129
<form

src/lib/components/ModelCardMetadata.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
import CarbonEarth from "~icons/carbon/earth";
33
import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
44
import BIMeta from "~icons/bi/meta";
5+
import CarbonCode from "~icons/carbon/code";
56
import type { Model } from "$lib/types/Model";
67
7-
export let model: Pick<Model, "name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl">;
8+
export let model: Pick<
9+
Model,
10+
"name" | "datasetName" | "websiteUrl" | "modelUrl" | "datasetUrl" | "hasInferenceAPI"
11+
>;
812
913
export let variant: "light" | "dark" = "light";
1014
</script>
@@ -35,6 +39,16 @@
3539
<div class="max-sm:hidden">&nbsp;page</div></a
3640
>
3741
{/if}
42+
{#if model.hasInferenceAPI}
43+
<a
44+
href={"https://huggingface.co/playground?modelId=" + model.name}
45+
target="_blank"
46+
rel="noreferrer"
47+
class="flex items-center hover:underline"
48+
><CarbonCode class="mr-1.5 shrink-0 text-xs text-gray-400" />
49+
API
50+
</a>
51+
{/if}
3852
{#if model.websiteUrl}
3953
<a
4054
href={model.websiteUrl}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<script lang="ts">
2+
import { fade } from "svelte/transition";
3+
import { onDestroy } from "svelte";
4+
import IconChevron from "./icons/IconChevron.svelte";
5+
6+
export let scrollNode: HTMLElement;
7+
export { className as class };
8+
9+
let visible = false;
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 =
28+
Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight &&
29+
scrollNode.scrollTop > 200;
30+
}
31+
32+
function scrollToPrevious() {
33+
if (!scrollNode) return;
34+
const messages = scrollNode.querySelectorAll('[id^="message-"]');
35+
const scrollTop = scrollNode.scrollTop;
36+
let previousMessage: Element | null = null;
37+
38+
for (let i = messages.length - 1; i >= 0; i--) {
39+
const messageTop =
40+
messages[i].getBoundingClientRect().top +
41+
scrollTop -
42+
scrollNode.getBoundingClientRect().top;
43+
if (messageTop < scrollTop - 1) {
44+
previousMessage = messages[i];
45+
break;
46+
}
47+
}
48+
49+
if (previousMessage) {
50+
previousMessage.scrollIntoView({ behavior: "smooth", block: "start" });
51+
}
52+
}
53+
54+
function destroy() {
55+
observer?.disconnect();
56+
scrollNode?.removeEventListener("scroll", updateVisibility);
57+
}
58+
59+
onDestroy(destroy);
60+
</script>
61+
62+
{#if visible}
63+
<button
64+
transition:fade={{ duration: 150 }}
65+
on:click={scrollToPrevious}
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]" />
69+
</button>
70+
{/if}

src/lib/components/chat/ChatInput.svelte

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { isDesktop } from "$lib/utils/isDesktop";
2+
import { browser } from "$app/environment";
33
import { createEventDispatcher, onMount } from "svelte";
44
55
export let value = "";
@@ -13,25 +13,41 @@
1313
1414
const dispatch = createEventDispatcher<{ submit: void }>();
1515
16+
function isVirtualKeyboard(): boolean {
17+
if (!browser) return false;
18+
19+
// Check for touch capability
20+
if (navigator.maxTouchPoints > 0) return true;
21+
22+
// Check for touch events
23+
if ("ontouchstart" in window) return true;
24+
25+
// Fallback to user agent string check
26+
const userAgent = navigator.userAgent.toLowerCase();
27+
28+
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
29+
}
30+
1631
$: minHeight = `${1 + minRows * 1.5}em`;
1732
$: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
1833
1934
function handleKeydown(event: KeyboardEvent) {
20-
// submit on enter
2135
if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
2236
event.preventDefault();
23-
// blur to close keyboard on mobile
24-
textareaElement.blur();
25-
// refocus so that user on desktop can start typing without needing to reclick on textarea
26-
if (isDesktop(window)) {
27-
textareaElement.focus();
37+
if (isVirtualKeyboard()) {
38+
// Insert a newline at the cursor position
39+
const start = textareaElement.selectionStart;
40+
const end = textareaElement.selectionEnd;
41+
value = value.substring(0, start) + "\n" + value.substring(end);
42+
textareaElement.selectionStart = textareaElement.selectionEnd = start + 1;
43+
} else {
44+
dispatch("submit");
2845
}
29-
dispatch("submit"); // use a custom event instead of `event.target.form.requestSubmit()` as it does not work on Safari 14
3046
}
3147
}
3248
3349
onMount(() => {
34-
if (isDesktop(window)) {
50+
if (!isVirtualKeyboard()) {
3551
textareaElement.focus();
3652
}
3753
});
@@ -44,7 +60,7 @@
4460
style="min-height: {minHeight}; max-height: {maxHeight}">{(value || " ") + "\n"}</pre>
4561

4662
<textarea
47-
enterkeyhint="send"
63+
enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
4864
tabindex="0"
4965
rows="1"
5066
class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0"

src/lib/components/chat/ChatMessage.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
{#if message.from === "assistant"}
231231
<div
232232
class="group relative -mb-4 flex items-start justify-start gap-4 pb-4 leading-relaxed"
233+
id="message-assistant-{message.id}"
233234
role="presentation"
234235
on:click={() => (isTapped = !isTapped)}
235236
on:keydown={() => (isTapped = !isTapped)}
@@ -372,6 +373,7 @@
372373
{#if message.from === "user"}
373374
<div
374375
class="group relative w-full items-start justify-start gap-4 max-sm:text-sm"
376+
id="message-user-{message.id}"
375377
role="presentation"
376378
on:click={() => (isTapped = !isTapped)}
377379
on:keydown={() => (isTapped = !isTapped)}

src/lib/components/chat/ChatWindow.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import AssistantIntroduction from "./AssistantIntroduction.svelte";
2828
import ChatMessage from "./ChatMessage.svelte";
2929
import ScrollToBottomBtn from "../ScrollToBottomBtn.svelte";
30+
import ScrollToPreviousBtn from "../ScrollToPreviousBtn.svelte";
3031
import { browser } from "$app/environment";
3132
import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
3233
import SystemPromptModal from "../SystemPromptModal.svelte";
@@ -329,8 +330,14 @@
329330
/>
330331
{/if}
331332
</div>
333+
334+
<ScrollToPreviousBtn
335+
class="fixed right-4 max-md:bottom-[calc(50%+26px)] md:bottom-48 lg:right-10"
336+
scrollNode={chatContainer}
337+
/>
338+
332339
<ScrollToBottomBtn
333-
class="bottom-36 right-4 max-md:hidden lg:right-10"
340+
class="fixed right-4 max-md:bottom-[calc(50%-26px)] md:bottom-36 lg:right-10"
334341
scrollNode={chatContainer}
335342
/>
336343
</div>

src/lib/server/endpoints/cohere/endpointCohere.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function endpointCohere(
6262
});
6363

6464
stream = await cohere.chatStream({
65+
forceSingleStep: true,
6566
message: prompt,
6667
rawPrompting: true,
6768
model: model.id ?? model.name,
@@ -82,6 +83,7 @@ export async function endpointCohere(
8283

8384
stream = await cohere
8485
.chatStream({
86+
forceSingleStep: true,
8587
model: model.id ?? model.name,
8688
chatHistory: formattedMessages.slice(0, -1),
8789
message: formattedMessages[formattedMessages.length - 1].message,

src/lib/server/endpoints/google/endpointVertex.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const endpointVertexParametersSchema = z.object({
1616
model: z.any(), // allow optional and validate against emptiness
1717
type: z.literal("vertex"),
1818
location: z.string().default("europe-west1"),
19+
extraBody: z.object({ model_version: z.string() }).optional(),
1920
project: z.string(),
2021
apiEndpoint: z.string().optional(),
2122
safetyThreshold: z
@@ -49,7 +50,7 @@ export const endpointVertexParametersSchema = z.object({
4950
});
5051

5152
export function endpointVertex(input: z.input<typeof endpointVertexParametersSchema>): Endpoint {
52-
const { project, location, model, apiEndpoint, safetyThreshold, tools, multimodal } =
53+
const { project, location, model, apiEndpoint, safetyThreshold, tools, multimodal, extraBody } =
5354
endpointVertexParametersSchema.parse(input);
5455

5556
const vertex_ai = new VertexAI({
@@ -64,7 +65,7 @@ export function endpointVertex(input: z.input<typeof endpointVertexParametersSch
6465
const hasFiles = messages.some((message) => message.files && message.files.length > 0);
6566

6667
const generativeModel = vertex_ai.getGenerativeModel({
67-
model: model.id ?? model.name,
68+
model: extraBody?.model_version ?? model.id ?? model.name,
6869
safetySettings: safetyThreshold
6970
? [
7071
{

src/lib/server/endpoints/tgi/endpointTgi.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export const endpointTgiParametersSchema = z.object({
2323
supportedMimeTypes: ["image/jpeg", "image/webp"],
2424
preferredMimeType: "image/webp",
2525
maxSizeInMB: 5,
26-
maxWidth: 224,
27-
maxHeight: 224,
26+
maxWidth: 378,
27+
maxHeight: 980,
2828
}),
2929
})
3030
.default({}),
@@ -81,22 +81,13 @@ export function endpointTgi(input: z.input<typeof endpointTgiParametersSchema>):
8181
};
8282
}
8383

84-
const whiteImage = {
85-
mime: "image/png",
86-
image: Buffer.from(
87-
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKACgD/2Q==",
88-
"base64"
89-
),
90-
};
91-
9284
async function prepareMessage(
9385
isMultimodal: boolean,
9486
message: EndpointMessage,
9587
imageProcessor: ImageProcessor
9688
): Promise<EndpointMessage> {
9789
if (!isMultimodal) return message;
98-
99-
const files = await Promise.all(message.files?.map(imageProcessor) ?? [whiteImage]);
90+
const files = await Promise.all(message.files?.map(imageProcessor) ?? []);
10091
const markdowns = files.map(
10192
(file) => `![](data:${file.mime};base64,${file.image.toString("base64")})`
10293
);

src/lib/server/models.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import JSON5 from "json5";
1313
import { getTokenizer } from "$lib/utils/getTokenizer";
1414
import { logger } from "$lib/server/logger";
1515
import { ToolResultStatus, type ToolInput } from "$lib/types/Tool";
16+
import { isHuggingChat } from "$lib/utils/isHuggingChat";
1617

1718
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
1819

@@ -253,10 +254,6 @@ const processModel = async (m: z.infer<typeof modelConfig>) => ({
253254
parameters: { ...m.parameters, stop_sequences: m.parameters?.stop },
254255
});
255256

256-
export type ProcessedModel = Awaited<ReturnType<typeof processModel>> & {
257-
getEndpoint: () => Promise<Endpoint>;
258-
};
259-
260257
const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
261258
...m,
262259
getEndpoint: async (): Promise<Endpoint> => {
@@ -316,10 +313,40 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
316313
},
317314
});
318315

319-
export const models: ProcessedModel[] = await Promise.all(
320-
modelsRaw.map((e) => processModel(e).then(addEndpoint))
316+
const hasInferenceAPI = async (m: Awaited<ReturnType<typeof processModel>>) => {
317+
if (!isHuggingChat) {
318+
return false;
319+
}
320+
321+
const r = await fetch(`https://huggingface.co/api/models/${m.id}`);
322+
323+
if (!r.ok) {
324+
logger.warn(`Failed to check if ${m.id} has inference API: ${r.statusText}`);
325+
return false;
326+
}
327+
328+
const json = await r.json();
329+
330+
if (json.cardData.inference === false) {
331+
return false;
332+
}
333+
334+
return true;
335+
};
336+
337+
export const models = await Promise.all(
338+
modelsRaw.map((e) =>
339+
processModel(e)
340+
.then(addEndpoint)
341+
.then(async (m) => ({
342+
...m,
343+
hasInferenceAPI: await hasInferenceAPI(m),
344+
}))
345+
)
321346
);
322347

348+
export type ProcessedModel = (typeof models)[number];
349+
323350
// super ugly but not sure how to make typescript happier
324351
export const validModelIdSchema = z.enum(models.map((m) => m.id) as [string, ...string[]]);
325352

@@ -357,5 +384,5 @@ export const smallModel = env.TASK_MODEL
357384

358385
export type BackendModel = Optional<
359386
typeof defaultModel,
360-
"preprompt" | "parameters" | "multimodal" | "unlisted" | "tools"
387+
"preprompt" | "parameters" | "multimodal" | "unlisted" | "tools" | "hasInferenceAPI"
361388
>;

0 commit comments

Comments
 (0)