├── .dockerignore ├── .env ├── .env.ci ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report--chat-ui-.md │ ├── config-support.md │ ├── feature-request--chat-ui-.md │ └── huggingchat.md ├── release.yml └── workflows │ ├── build-docs.yml │ ├── build-image.yml │ ├── build-pr-docs.yml │ ├── deploy-prod.yml │ ├── lint-and-test.yml │ ├── trufflehog.yml │ └── upload-pr-documentation.yml ├── .gitignore ├── .husky ├── lint-stage-config.js └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── PRIVACY.md ├── PROMPTS.md ├── README.md ├── chart ├── Chart.yaml ├── env │ └── prod.yaml ├── templates │ ├── _helpers.tpl │ ├── config.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── infisical.yaml │ ├── ingress-internal.yaml │ ├── ingress.yaml │ ├── network-policy.yaml │ ├── service-account.yaml │ ├── service-monitor.yaml │ └── service.yaml └── values.yaml ├── docs └── source │ ├── _toctree.yml │ ├── configuration │ ├── common-issues.md │ ├── embeddings.md │ ├── metrics.md │ ├── models │ │ ├── multimodal.md │ │ ├── overview.md │ │ ├── providers │ │ │ ├── anthropic.md │ │ │ ├── aws.md │ │ │ ├── cloudflare.md │ │ │ ├── cohere.md │ │ │ ├── google.md │ │ │ ├── langserve.md │ │ │ ├── llamacpp.md │ │ │ ├── ollama.md │ │ │ ├── openai.md │ │ │ └── tgi.md │ │ └── tools.md │ ├── open-id.md │ ├── overview.md │ ├── theming.md │ └── web-search.md │ ├── developing │ ├── architecture.md │ └── copy-huggingchat.md │ ├── index.md │ └── installation │ ├── docker.md │ ├── helm.md │ ├── local.md │ └── spaces.md ├── entrypoint.sh ├── models └── add-your-models-here.txt ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts ├── config.ts ├── populate.ts ├── samples.txt ├── setups │ ├── vitest-setup-client.ts │ └── vitest-setup-server.ts └── updateLocalEnv.ts ├── src ├── ambient.d.ts ├── app.d.ts ├── app.html ├── hooks.server.ts ├── lib │ ├── actions │ │ ├── clickOutside.ts │ │ └── snapScrollToBottom.ts │ ├── buildPrompt.ts │ ├── components │ │ ├── AnnouncementBanner.svelte │ │ ├── AssistantSettings.svelte │ │ ├── AssistantToolPicker.svelte │ │ ├── CodeBlock.svelte │ │ ├── ContinueBtn.svelte │ │ ├── CopyToClipBoardBtn.svelte │ │ ├── DisclaimerModal.svelte │ │ ├── ExpandNavigation.svelte │ │ ├── HoverTooltip.svelte │ │ ├── InfiniteScroll.svelte │ │ ├── LoginModal.svelte │ │ ├── MobileNav.svelte │ │ ├── Modal.svelte │ │ ├── ModelCardMetadata.svelte │ │ ├── NavConversationItem.svelte │ │ ├── NavMenu.svelte │ │ ├── OpenWebSearchResults.svelte │ │ ├── OverloadedModal.svelte │ │ ├── Pagination.svelte │ │ ├── PaginationArrow.svelte │ │ ├── Portal.svelte │ │ ├── RetryBtn.svelte │ │ ├── ScrollToBottomBtn.svelte │ │ ├── ScrollToPreviousBtn.svelte │ │ ├── StopGeneratingBtn.svelte │ │ ├── Switch.svelte │ │ ├── SystemPromptModal.svelte │ │ ├── Toast.svelte │ │ ├── TokensCounter.svelte │ │ ├── ToolBadge.svelte │ │ ├── ToolLogo.svelte │ │ ├── ToolsMenu.svelte │ │ ├── Tooltip.svelte │ │ ├── UploadBtn.svelte │ │ ├── WebSearchToggle.svelte │ │ ├── chat │ │ │ ├── Alternatives.svelte │ │ │ ├── AssistantIntroduction.svelte │ │ │ ├── ChatInput.svelte │ │ │ ├── ChatIntroduction.svelte │ │ │ ├── ChatMessage.svelte │ │ │ ├── ChatWindow.svelte │ │ │ ├── FileDropzone.svelte │ │ │ ├── MarkdownRenderer.svelte │ │ │ ├── MarkdownRenderer.svelte.test.ts │ │ │ ├── ModelSwitch.svelte │ │ │ ├── OpenReasoningResults.svelte │ │ │ ├── Search.svelte │ │ │ ├── ToolUpdate.svelte │ │ │ ├── UploadedFile.svelte │ │ │ └── Vote.svelte │ │ ├── icons │ │ │ ├── IconChevron.svelte │ │ │ ├── IconCopy.svelte │ │ │ ├── IconDazzled.svelte │ │ │ ├── IconImageGen.svelte │ │ │ ├── IconInternet.svelte │ │ │ ├── IconLoading.svelte │ │ │ ├── IconNew.svelte │ │ │ ├── IconPaperclip.svelte │ │ │ ├── IconScreenshot.svelte │ │ │ ├── IconTool.svelte │ │ │ ├── Logo.svelte │ │ │ └── LogoHuggingFaceBorderless.svelte │ │ └── players │ │ │ └── AudioPlayer.svelte │ ├── constants │ │ ├── pagination.ts │ │ └── publicSepToken.ts │ ├── jobs │ │ ├── refresh-assistants-counts.ts │ │ └── refresh-conversation-stats.ts │ ├── migrations │ │ ├── lock.ts │ │ ├── migrations.spec.ts │ │ ├── migrations.ts │ │ └── routines │ │ │ ├── 01-update-search-assistants.ts │ │ │ ├── 02-update-assistants-models.ts │ │ │ ├── 03-add-tools-in-settings.ts │ │ │ ├── 04-update-message-updates.ts │ │ │ ├── 05-update-message-files.ts │ │ │ ├── 06-trim-message-updates.ts │ │ │ ├── 07-reset-tools-in-settings.ts │ │ │ ├── 08-update-featured-to-review.ts │ │ │ ├── 09-delete-empty-conversations.spec.ts │ │ │ ├── 09-delete-empty-conversations.ts │ │ │ └── index.ts │ ├── server │ │ ├── abortedGenerations.ts │ │ ├── adminToken.ts │ │ ├── auth.ts │ │ ├── config.ts │ │ ├── database.ts │ │ ├── embeddingEndpoints │ │ │ ├── embeddingEndpoints.ts │ │ │ ├── hfApi │ │ │ │ └── embeddingHfApi.ts │ │ │ ├── openai │ │ │ │ └── embeddingEndpoints.ts │ │ │ ├── tei │ │ │ │ └── embeddingEndpoints.ts │ │ │ └── transformersjs │ │ │ │ └── embeddingEndpoints.ts │ │ ├── embeddingModels.ts │ │ ├── endpoints │ │ │ ├── anthropic │ │ │ │ ├── endpointAnthropic.ts │ │ │ │ ├── endpointAnthropicVertex.ts │ │ │ │ └── utils.ts │ │ │ ├── aws │ │ │ │ ├── endpointAws.ts │ │ │ │ └── endpointBedrock.ts │ │ │ ├── cloudflare │ │ │ │ └── endpointCloudflare.ts │ │ │ ├── cohere │ │ │ │ └── endpointCohere.ts │ │ │ ├── document.ts │ │ │ ├── endpoints.ts │ │ │ ├── google │ │ │ │ ├── endpointGenAI.ts │ │ │ │ └── endpointVertex.ts │ │ │ ├── images.ts │ │ │ ├── inference-client │ │ │ │ └── endpointInferenceClient.ts │ │ │ ├── langserve │ │ │ │ └── endpointLangserve.ts │ │ │ ├── llamacpp │ │ │ │ └── endpointLlamacpp.ts │ │ │ ├── local │ │ │ │ └── endpointLocal.ts │ │ │ ├── ollama │ │ │ │ └── endpointOllama.ts │ │ │ ├── openai │ │ │ │ ├── endpointOai.ts │ │ │ │ ├── openAIChatToTextGenerationStream.ts │ │ │ │ └── openAICompletionToTextGenerationStream.ts │ │ │ ├── preprocessMessages.ts │ │ │ └── tgi │ │ │ │ └── endpointTgi.ts │ │ ├── exitHandler.ts │ │ ├── files │ │ │ ├── downloadFile.ts │ │ │ └── uploadFile.ts │ │ ├── findRepoRoot.ts │ │ ├── fonts │ │ │ ├── Inter-Black.ttf │ │ │ ├── Inter-Bold.ttf │ │ │ ├── Inter-ExtraBold.ttf │ │ │ ├── Inter-ExtraLight.ttf │ │ │ ├── Inter-Light.ttf │ │ │ ├── Inter-Medium.ttf │ │ │ ├── Inter-Regular.ttf │ │ │ ├── Inter-SemiBold.ttf │ │ │ └── Inter-Thin.ttf │ │ ├── generateFromDefaultEndpoint.ts │ │ ├── isURLLocal.spec.ts │ │ ├── isURLLocal.ts │ │ ├── logger.ts │ │ ├── metrics.ts │ │ ├── models.ts │ │ ├── sendSlack.ts │ │ ├── sentenceSimilarity.ts │ │ ├── textGeneration │ │ │ ├── assistant.ts │ │ │ ├── generate.ts │ │ │ ├── index.ts │ │ │ ├── reasoning.ts │ │ │ ├── title.ts │ │ │ ├── tools.ts │ │ │ └── types.ts │ │ ├── tools │ │ │ ├── calculator.ts │ │ │ ├── directlyAnswer.ts │ │ │ ├── getToolOutput.ts │ │ │ ├── index.ts │ │ │ ├── outputs.ts │ │ │ ├── utils.ts │ │ │ └── web │ │ │ │ ├── search.ts │ │ │ │ └── url.ts │ │ ├── usageLimits.ts │ │ └── websearch │ │ │ ├── embed │ │ │ ├── combine.ts │ │ │ ├── embed.ts │ │ │ └── tree.ts │ │ │ ├── markdown │ │ │ ├── fromHtml.ts │ │ │ ├── tree.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── chunk.ts │ │ │ │ ├── nlp.ts │ │ │ │ └── stringify.ts │ │ │ ├── runWebSearch.ts │ │ │ ├── scrape │ │ │ ├── parser.ts │ │ │ ├── playwright.ts │ │ │ ├── scrape.ts │ │ │ └── types.ts │ │ │ ├── search │ │ │ ├── endpoints.ts │ │ │ ├── endpoints │ │ │ │ ├── bing.ts │ │ │ │ ├── searchApi.ts │ │ │ │ ├── searxng.ts │ │ │ │ ├── serpApi.ts │ │ │ │ ├── serpStack.ts │ │ │ │ ├── serper.ts │ │ │ │ ├── webLocal.ts │ │ │ │ └── youApi.ts │ │ │ ├── generateQuery.ts │ │ │ └── search.ts │ │ │ └── update.ts │ ├── shareConversation.ts │ ├── stores │ │ ├── errors.ts │ │ ├── isAborted.ts │ │ ├── loginModal.ts │ │ ├── pendingMessage.ts │ │ ├── settings.ts │ │ ├── titleUpdate.ts │ │ └── webSearchParameters.ts │ ├── switchTheme.ts │ ├── types │ │ ├── AbortedGeneration.ts │ │ ├── Assistant.ts │ │ ├── AssistantStats.ts │ │ ├── ConfigKey.ts │ │ ├── ConvSidebar.ts │ │ ├── Conversation.ts │ │ ├── ConversationStats.ts │ │ ├── Message.ts │ │ ├── MessageEvent.ts │ │ ├── MessageUpdate.ts │ │ ├── MigrationResult.ts │ │ ├── Model.ts │ │ ├── Report.ts │ │ ├── Review.ts │ │ ├── Semaphore.ts │ │ ├── Session.ts │ │ ├── Settings.ts │ │ ├── SharedConversation.ts │ │ ├── Template.ts │ │ ├── Timestamps.ts │ │ ├── TokenCache.ts │ │ ├── Tool.ts │ │ ├── UrlDependency.ts │ │ ├── User.ts │ │ └── WebSearch.ts │ ├── utils │ │ ├── PublicConfig.svelte.ts │ │ ├── chunk.ts │ │ ├── cookiesAreEnabled.ts │ │ ├── debounce.ts │ │ ├── deepestChild.ts │ │ ├── file2base64.ts │ │ ├── formatUserCount.ts │ │ ├── getGradioApi.ts │ │ ├── getHref.ts │ │ ├── getReturnFromGenerator.ts │ │ ├── getShareUrl.ts │ │ ├── getTokenizer.ts │ │ ├── hashConv.ts │ │ ├── isDesktop.ts │ │ ├── isUrl.ts │ │ ├── isVirtualKeyboard.ts │ │ ├── marked.ts │ │ ├── mergeAsyncGenerators.ts │ │ ├── messageUpdates.ts │ │ ├── models.ts │ │ ├── parseStringToList.ts │ │ ├── randomUuid.ts │ │ ├── screenshot.ts │ │ ├── searchTokens.ts │ │ ├── sha256.ts │ │ ├── share.ts │ │ ├── stringifyError.ts │ │ ├── sum.ts │ │ ├── template.spec.ts │ │ ├── template.ts │ │ ├── timeout.ts │ │ ├── toolIds.ts │ │ ├── tools.ts │ │ ├── tree │ │ │ ├── addChildren.spec.ts │ │ │ ├── addChildren.ts │ │ │ ├── addSibling.spec.ts │ │ │ ├── addSibling.ts │ │ │ ├── buildSubtree.spec.ts │ │ │ ├── buildSubtree.ts │ │ │ ├── convertLegacyConversation.spec.ts │ │ │ ├── convertLegacyConversation.ts │ │ │ ├── isMessageId.spec.ts │ │ │ ├── isMessageId.ts │ │ │ └── treeHelpers.spec.ts │ │ └── updates.ts │ └── workers │ │ └── markdownWorker.ts ├── routes │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.svelte │ ├── admin │ │ ├── export │ │ │ └── +server.ts │ │ └── stats │ │ │ └── compute │ │ │ └── +server.ts │ ├── api │ │ ├── assistant │ │ │ ├── +server.ts │ │ │ ├── [id] │ │ │ │ ├── +server.ts │ │ │ │ ├── report │ │ │ │ │ └── +server.ts │ │ │ │ ├── review │ │ │ │ │ └── +server.ts │ │ │ │ └── subscribe │ │ │ │ │ └── +server.ts │ │ │ └── utils.ts │ │ ├── assistants │ │ │ └── +server.ts │ │ ├── conversation │ │ │ └── [id] │ │ │ │ ├── +server.ts │ │ │ │ └── message │ │ │ │ └── [messageId] │ │ │ │ └── +server.ts │ │ ├── conversations │ │ │ ├── +server.ts │ │ │ └── search │ │ │ │ └── +server.ts │ │ ├── models │ │ │ └── +server.ts │ │ ├── spaces-config │ │ │ └── +server.ts │ │ ├── tools │ │ │ ├── +server.ts │ │ │ ├── [toolId] │ │ │ │ ├── +server.ts │ │ │ │ ├── report │ │ │ │ │ └── +server.ts │ │ │ │ └── review │ │ │ │ │ └── +server.ts │ │ │ └── search │ │ │ │ └── +server.ts │ │ └── user │ │ │ ├── +server.ts │ │ │ ├── assistants │ │ │ └── +server.ts │ │ │ └── validate-token │ │ │ └── +server.ts │ ├── assistant │ │ └── [assistantId] │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── thumbnail.png │ │ │ ├── +server.ts │ │ │ └── ChatThumbnail.svelte │ ├── assistants │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── conversation │ │ ├── +server.ts │ │ └── [id] │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ ├── +server.ts │ │ │ ├── message │ │ │ └── [messageId] │ │ │ │ ├── prompt │ │ │ │ └── +server.ts │ │ │ │ └── vote │ │ │ │ └── +server.ts │ │ │ ├── output │ │ │ └── [sha256] │ │ │ │ └── +server.ts │ │ │ ├── share │ │ │ └── +server.ts │ │ │ └── stop-generating │ │ │ └── +server.ts │ ├── healthcheck │ │ └── +server.ts │ ├── login │ │ ├── +page.server.ts │ │ └── callback │ │ │ ├── +page.server.ts │ │ │ ├── updateUser.spec.ts │ │ │ └── updateUser.ts │ ├── logout │ │ └── +page.server.ts │ ├── models │ │ ├── +page.svelte │ │ └── [...model] │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── thumbnail.png │ │ │ ├── +server.ts │ │ │ └── ModelThumbnail.svelte │ ├── privacy │ │ └── +page.svelte │ ├── r │ │ └── [id] │ │ │ └── +page.ts │ ├── settings │ │ ├── (nav) │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.svelte │ │ │ ├── +server.ts │ │ │ ├── [...model] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ ├── application │ │ │ │ └── +page.svelte │ │ │ └── assistants │ │ │ │ ├── [assistantId] │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ ├── ReportModal.svelte │ │ │ │ ├── avatar.jpg │ │ │ │ │ └── +server.ts │ │ │ │ └── edit │ │ │ │ │ └── +page.svelte │ │ │ │ └── new │ │ │ │ └── +page.svelte │ │ ├── +layout.server.ts │ │ └── +layout.svelte │ └── tools │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── ToolEdit.svelte │ │ ├── ToolInputComponent.svelte │ │ ├── [toolId] │ │ ├── +layout.server.ts │ │ ├── +page.svelte │ │ └── edit │ │ │ └── +page.svelte │ │ └── new │ │ └── +page.svelte └── styles │ ├── highlight-js.css │ └── main.css ├── static ├── chatui │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── favicon.svg │ ├── icon-128x128.png │ ├── icon-256x256.png │ ├── icon-512x512.png │ ├── icon.svg │ ├── logo.svg │ └── manifest.json └── huggingchat │ ├── apple-touch-icon.png │ ├── assistants-thumbnail.png │ ├── favicon.ico │ ├── favicon.svg │ ├── icon-128x128.png │ ├── icon-144x144.png │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-36x36.png │ ├── icon-48x48.png │ ├── icon-512x512.png │ ├── icon-72x72.png │ ├── icon-96x96.png │ ├── icon.svg │ ├── logo.svg │ ├── manifest.json │ ├── thumbnail.png │ └── tools-thumbnail.png ├── stub └── @reflink │ └── reflink │ ├── index.js │ └── package.json ├── svelte.config.js ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .vscode/ 3 | .idea 4 | .gitignore 5 | LICENSE 6 | README.md 7 | node_modules/ 8 | .svelte-kit/ 9 | .env* 10 | !.env 11 | .env.local 12 | db 13 | models/** -------------------------------------------------------------------------------- /.env.ci: -------------------------------------------------------------------------------- 1 | MONGODB_URL=mongodb://localhost:27017/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:svelte/recommended", 8 | "prettier", 9 | ], 10 | plugins: ["@typescript-eslint"], 11 | ignorePatterns: ["*.cjs"], 12 | overrides: [ 13 | { 14 | files: ["*.svelte"], 15 | parser: "svelte-eslint-parser", 16 | parserOptions: { 17 | parser: "@typescript-eslint/parser", 18 | }, 19 | }, 20 | ], 21 | parserOptions: { 22 | sourceType: "module", 23 | ecmaVersion: 2020, 24 | extraFileExtensions: [".svelte"], 25 | }, 26 | rules: { 27 | "require-yield": "off", 28 | "@typescript-eslint/no-explicit-any": "error", 29 | "@typescript-eslint/no-non-null-assertion": "error", 30 | "@typescript-eslint/no-unused-vars": [ 31 | // prevent variables with a _ prefix from being marked as unused 32 | "error", 33 | { 34 | argsIgnorePattern: "^_", 35 | }, 36 | ], 37 | "object-shorthand": ["error", "always"], 38 | }, 39 | env: { 40 | browser: true, 41 | es2017: true, 42 | node: true, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report (chat-ui) 3 | about: Use this for confirmed issues with chat-ui 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | ## Bug description 10 | 11 | 12 | 13 | ## Steps to reproduce 14 | 15 | 16 | 17 | ## Screenshots 18 | 19 | 20 | 21 | ## Context 22 | 23 | ### Logs 24 | 25 | 26 | 27 | ``` 28 | // logs here if relevant 29 | ``` 30 | 31 | ### Specs 32 | 33 | - **OS**: 34 | - **Browser**: 35 | - **chat-ui commit**: 36 | 37 | ### Config 38 | 39 | 40 | 41 | ## Notes 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config-support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Config Support 3 | about: Help with setting up chat-ui locally 4 | title: "" 5 | labels: support 6 | assignees: "" 7 | --- 8 | 9 | **Please use the discussions on GitHub** for getting help with setting things up instead of opening an issue: https://github.com/huggingface/chat-ui/discussions 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request (chat-ui) 3 | about: Suggest new features to be added to chat-ui 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Describe your feature request 10 | 11 | 12 | 13 | ## Screenshots (if relevant) 14 | 15 | ## Implementation idea 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/huggingchat.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: HuggingChat 3 | about: Requests & reporting outages on HuggingChat, the hosted version of chat-ui. 4 | title: "" 5 | labels: huggingchat 6 | assignees: "" 7 | --- 8 | 9 | **Do not use GitHub issues** for requesting models on HuggingChat or reporting issues with HuggingChat being down/overloaded. 10 | 11 | **Use the discussions page on the hub instead:** https://huggingface.co/spaces/huggingchat/chat-ui/discussions 12 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - huggingchat 5 | - CI/CD 6 | - documentation 7 | categories: 8 | - title: Features 9 | labels: 10 | - enhancement 11 | - title: Bugfixes 12 | labels: 13 | - bug 14 | - title: Other changes 15 | labels: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - v*-release 8 | 9 | jobs: 10 | build: 11 | uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main 12 | with: 13 | commit_sha: ${{ github.sha }} 14 | package: chat-ui 15 | additional_args: --not_python_module 16 | secrets: 17 | token: ${{ secrets.HUGGINGFACE_PUSH }} 18 | hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} 19 | -------------------------------------------------------------------------------- /.github/workflows/build-pr-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build PR Documentation 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "docs/source/**" 7 | - ".github/workflows/build-pr-docs.yml" 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main 16 | with: 17 | commit_sha: ${{ github.event.pull_request.head.sha }} 18 | pr_number: ${{ github.event.number }} 19 | package: chat-ui 20 | additional_args: --not_python_module 21 | -------------------------------------------------------------------------------- /.github/workflows/trufflehog.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | 4 | name: Secret Leaks 5 | 6 | jobs: 7 | trufflehog: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Secret Scanning 15 | uses: trufflesecurity/trufflehog@main 16 | with: 17 | extra_args: --results=verified,unknown 18 | -------------------------------------------------------------------------------- /.github/workflows/upload-pr-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Upload PR Documentation 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Build PR Documentation"] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | build: 11 | uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main 12 | with: 13 | package_name: chat-ui 14 | secrets: 15 | hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} 16 | comment_bot_token: ${{ secrets.COMMENT_BOT_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | vite.config.js.timestamp-* 9 | vite.config.ts.timestamp-* 10 | SECRET_CONFIG 11 | .idea 12 | !.env.ci 13 | !.env 14 | gcp-*.json 15 | db 16 | models/* 17 | !models/add-your-models-here.txt -------------------------------------------------------------------------------- /.husky/lint-stage-config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"], 3 | "*.json": ["prettier --write"], 4 | }; 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | set -e 2 | npx lint-staged --config ./.husky/lint-stage-config.js 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | /chart 7 | .env 8 | .env.* 9 | !.env.example 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | pnpm-lock.yaml 13 | package-lock.json 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 6 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "npm run dev", 6 | "name": "Run development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit" 6 | }, 7 | "eslint.validate": ["javascript", "svelte"], 8 | "[svelte]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[typescript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: chat-ui 3 | version: 0.0.1-latest 4 | type: application 5 | icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg 6 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "name" -}} 2 | {{- default $.Release.Name | trunc 63 | trimSuffix "-" -}} 3 | {{- end -}} 4 | 5 | {{- define "app.name" -}} 6 | chat-ui 7 | {{- end -}} 8 | 9 | {{- define "labels.standard" -}} 10 | release: {{ $.Release.Name | quote }} 11 | heritage: {{ $.Release.Service | quote }} 12 | chart: "{{ include "name" . }}" 13 | app: "{{ include "app.name" . }}" 14 | {{- end -}} 15 | 16 | {{- define "labels.resolver" -}} 17 | release: {{ $.Release.Name | quote }} 18 | heritage: {{ $.Release.Service | quote }} 19 | chart: "{{ include "name" . }}" 20 | app: "{{ include "app.name" . }}-resolver" 21 | {{- end -}} 22 | 23 | -------------------------------------------------------------------------------- /chart/templates/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: {{ include "labels.standard" . | nindent 4 }} 5 | name: {{ include "name" . }} 6 | namespace: {{ .Release.Namespace }} 7 | data: 8 | {{- range $key, $value := $.Values.envVars }} 9 | {{ $key }}: {{ $value | quote }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /chart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | labels: {{ include "labels.standard" . | nindent 4 }} 6 | name: {{ include "name" . }} 7 | namespace: {{ .Release.Namespace }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "name" . }} 13 | minReplicas: {{ $.Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ $.Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if ne "" $.Values.autoscaling.targetMemoryUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: memory 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ $.Values.autoscaling.targetMemoryUtilizationPercentage | int }} 23 | {{- end }} 24 | {{- if ne "" $.Values.autoscaling.targetCPUUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: cpu 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ $.Values.autoscaling.targetCPUUtilizationPercentage | int }} 31 | {{- end }} 32 | behavior: 33 | scaleDown: 34 | stabilizationWindowSeconds: 600 35 | policies: 36 | - type: Percent 37 | value: 10 38 | periodSeconds: 60 39 | scaleUp: 40 | stabilizationWindowSeconds: 0 41 | policies: 42 | - type: Pods 43 | value: 1 44 | periodSeconds: 30 45 | {{- end }} 46 | -------------------------------------------------------------------------------- /chart/templates/infisical.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.infisical.enabled }} 2 | apiVersion: secrets.infisical.com/v1alpha1 3 | kind: InfisicalSecret 4 | metadata: 5 | name: {{ include "name" $ }}-infisical-secret 6 | namespace: {{ $.Release.Namespace }} 7 | spec: 8 | authentication: 9 | universalAuth: 10 | credentialsRef: 11 | secretName: {{ .Values.infisical.operatorSecretName | quote }} 12 | secretNamespace: {{ .Values.infisical.operatorSecretNamespace | quote }} 13 | secretsScope: 14 | envSlug: {{ .Values.infisical.env | quote }} 15 | projectSlug: {{ .Values.infisical.project | quote }} 16 | secretsPath: / 17 | hostAPI: {{ .Values.infisical.url | quote }} 18 | managedSecretReference: 19 | creationPolicy: Owner 20 | secretName: {{ include "name" $ }}-secs 21 | secretNamespace: {{ .Release.Namespace | quote }} 22 | secretType: Opaque 23 | resyncInterval: {{ .Values.infisical.resyncInterval }} 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /chart/templates/ingress-internal.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.ingressInternal.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | annotations: {{ toYaml .Values.ingressInternal.annotations | nindent 4 }} 6 | labels: {{ include "labels.standard" . | nindent 4 }} 7 | name: {{ include "name" . }}-internal 8 | namespace: {{ .Release.Namespace }} 9 | spec: 10 | {{ if $.Values.ingressInternal.className }} 11 | ingressClassName: {{ .Values.ingressInternal.className }} 12 | {{ end }} 13 | {{- with .Values.ingressInternal.tls }} 14 | tls: 15 | - hosts: 16 | - {{ $.Values.domain | quote }} 17 | {{- with .secretName }} 18 | secretName: {{ . }} 19 | {{- end }} 20 | {{- end }} 21 | rules: 22 | - host: {{ .Values.domain }} 23 | http: 24 | paths: 25 | - backend: 26 | service: 27 | name: {{ include "name" . }} 28 | port: 29 | name: http 30 | path: {{ $.Values.ingressInternal.path | default "/" }} 31 | pathType: Prefix 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | annotations: {{ toYaml .Values.ingress.annotations | nindent 4 }} 6 | labels: {{ include "labels.standard" . | nindent 4 }} 7 | name: {{ include "name" . }} 8 | namespace: {{ .Release.Namespace }} 9 | spec: 10 | {{ if $.Values.ingress.className }} 11 | ingressClassName: {{ .Values.ingress.className }} 12 | {{ end }} 13 | {{- with .Values.ingress.tls }} 14 | tls: 15 | - hosts: 16 | - {{ $.Values.domain | quote }} 17 | {{- with .secretName }} 18 | secretName: {{ . }} 19 | {{- end }} 20 | {{- end }} 21 | rules: 22 | - host: {{ .Values.domain }} 23 | http: 24 | paths: 25 | - backend: 26 | service: 27 | name: {{ include "name" . }} 28 | port: 29 | name: http 30 | path: {{ $.Values.ingress.path | default "/" }} 31 | pathType: Prefix 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /chart/templates/network-policy.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ include "name" . }} 6 | namespace: {{ .Release.Namespace }} 7 | spec: 8 | egress: 9 | - ports: 10 | - port: 53 11 | protocol: UDP 12 | to: 13 | - namespaceSelector: 14 | matchLabels: 15 | kubernetes.io/metadata.name: kube-system 16 | podSelector: 17 | matchLabels: 18 | k8s-app: kube-dns 19 | - to: 20 | {{- range $ip := .Values.networkPolicy.allowedBlocks }} 21 | - ipBlock: 22 | cidr: {{ $ip | quote }} 23 | {{- end }} 24 | - to: 25 | - ipBlock: 26 | cidr: 0.0.0.0/0 27 | except: 28 | - 10.0.0.0/8 29 | - 172.16.0.0/12 30 | - 192.168.0.0/16 31 | - 169.254.169.254/32 32 | podSelector: 33 | matchLabels: {{ include "labels.standard" . | nindent 6 }} 34 | policyTypes: 35 | - Egress 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /chart/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.serviceAccount.enabled .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} 5 | metadata: 6 | name: "{{ .Values.serviceAccount.name | default (include "name" .) }}" 7 | namespace: {{ .Release.Namespace }} 8 | labels: {{ include "labels.standard" . | nindent 4 }} 9 | {{- with .Values.serviceAccount.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /chart/templates/service-monitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.monitoring.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: {{ include "labels.standard" . | nindent 4 }} 6 | name: {{ include "name" . }} 7 | namespace: {{ .Release.Namespace }} 8 | spec: 9 | selector: 10 | matchLabels: {{ include "labels.standard" . | nindent 6 }} 11 | endpoints: 12 | - port: metrics 13 | path: /metrics 14 | interval: 15s 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "{{ include "name" . }}" 5 | annotations: {{ toYaml .Values.service.annotations | nindent 4 }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: {{ include "labels.standard" . | nindent 4 }} 8 | spec: 9 | ports: 10 | - name: http 11 | port: 80 12 | protocol: TCP 13 | targetPort: http 14 | {{- if $.Values.monitoring.enabled }} 15 | - name: metrics 16 | port: 5565 17 | protocol: TCP 18 | targetPort: metrics 19 | {{- end }} 20 | selector: {{ include "labels.standard" . | nindent 4 }} 21 | type: {{.Values.service.type}} 22 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: ghcr.io/huggingface 3 | name: chat-ui 4 | tag: 0.0.0-latest 5 | pullPolicy: IfNotPresent 6 | 7 | replicas: 3 8 | 9 | domain: huggingface.co 10 | 11 | networkPolicy: 12 | enabled: false 13 | allowedBlocks: [] 14 | 15 | service: 16 | type: NodePort 17 | annotations: { } 18 | 19 | serviceAccount: 20 | enabled: false 21 | create: false 22 | name: "" 23 | automountServiceAccountToken: true 24 | annotations: { } 25 | 26 | ingress: 27 | enabled: true 28 | path: "/" 29 | annotations: { } 30 | # className: "nginx" 31 | tls: { } 32 | # secretName: XXX 33 | 34 | ingressInternal: 35 | enabled: false 36 | path: "/" 37 | annotations: { } 38 | # className: "nginx" 39 | tls: { } 40 | 41 | resources: 42 | requests: 43 | cpu: 2 44 | memory: 4Gi 45 | limits: 46 | cpu: 2 47 | memory: 4Gi 48 | nodeSelector: {} 49 | tolerations: [] 50 | 51 | envVars: { } 52 | 53 | infisical: 54 | enabled: false 55 | env: "" 56 | project: "huggingchat-v2-a1" 57 | url: "" 58 | resyncInterval: 60 59 | operatorSecretName: "huggingchat-operator-secrets" 60 | operatorSecretNamespace: "hub-utils" 61 | 62 | # Allow to environment injections on top or instead of infisical 63 | extraEnvFrom: [] 64 | extraEnv: [] 65 | 66 | autoscaling: 67 | enabled: false 68 | minReplicas: 1 69 | maxReplicas: 2 70 | targetMemoryUtilizationPercentage: "" 71 | targetCPUUtilizationPercentage: "" 72 | 73 | monitoring: 74 | enabled: false 75 | -------------------------------------------------------------------------------- /docs/source/configuration/common-issues.md: -------------------------------------------------------------------------------- 1 | # Common Issues 2 | 3 | ## 403:You don't have access to this conversation 4 | 5 | Most likely you are running chat-ui over HTTP. The recommended option is to setup something like NGINX to handle HTTPS and proxy the requests to chat-ui. If you really need to run over HTTP you can add `ALLOW_INSECURE_COOKIES=true` to your `.env.local`. 6 | 7 | Make sure to set your `PUBLIC_ORIGIN` in your `.env.local` to the correct URL as well. 8 | -------------------------------------------------------------------------------- /docs/source/configuration/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | The server can expose prometheus metrics on port `5565` but is off by default. You may enable the metrics server with `METRICS_ENABLED=true` and change the port with `METRICS_PORT=1234`. 4 | 5 | 6 | 7 | In development with `npm run dev`, the metrics server does not shutdown gracefully due to Sveltekit not providing hooks for restart. It's recommended to disable the metrics server in this case. 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/configuration/models/multimodal.md: -------------------------------------------------------------------------------- 1 | # Multimodal 2 | 3 | We currently support [IDEFICS](https://huggingface.co/blog/idefics) (hosted on [TGI](./providers/tgi)), OpenAI and Anthropic Claude 3 as multimodal models. You can enable it by setting `multimodal: true` in your `MODELS` configuration. For IDEFICS, you must have a [PRO HF Api token](https://huggingface.co/settings/tokens). For OpenAI, see the [OpenAI section](./providers/openai). For Anthropic, see the [Anthropic section](./providers/anthropic). 4 | 5 | ```ini 6 | MODELS=`[ 7 | { 8 | "name": "HuggingFaceM4/idefics-80b-instruct", 9 | "multimodal" : true, 10 | "description": "IDEFICS is the new multimodal model by Hugging Face.", 11 | "preprompt": "", 12 | "chatPromptTemplate" : "{{#each messages}}{{#ifUser}}User: {{content}}{{/ifUser}}\nAssistant: {{#ifAssistant}}{{content}}\n{{/ifAssistant}}{{/each}}", 13 | "parameters": { 14 | "temperature": 0.1, 15 | "top_p": 0.95, 16 | "repetition_penalty": 1.2, 17 | "top_k": 12, 18 | "truncate": 1000, 19 | "max_new_tokens": 1024, 20 | "stop": ["", "User:", "\nUser:"] 21 | } 22 | } 23 | ]` 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/source/configuration/models/providers/aws.md: -------------------------------------------------------------------------------- 1 | # Amazon Web Services (AWS) 2 | 3 | | Feature | Available | 4 | | --------------------------- | --------- | 5 | | [Tools](../tools) | No | 6 | | [Multimodal](../multimodal) | No | 7 | 8 | You may specify your Amazon SageMaker instance as an endpoint for Chat UI: 9 | 10 | ```ini 11 | MODELS=`[{ 12 | "name": "your-model", 13 | "displayName": "Your Model", 14 | "description": "Your description", 15 | "parameters": { 16 | "max_new_tokens": 4096 17 | }, 18 | "endpoints": [ 19 | { 20 | "type" : "aws", 21 | "service" : "sagemaker" 22 | "url": "", 23 | "accessKey": "", 24 | "secretKey" : "", 25 | "sessionToken": "", 26 | "region": "", 27 | "weight": 1 28 | } 29 | ] 30 | }]` 31 | ``` 32 | 33 | You can also set `"service": "lambda"` to use a lambda instance. 34 | 35 | You can get the `accessKey` and `secretKey` from your AWS user, under programmatic access. 36 | -------------------------------------------------------------------------------- /docs/source/configuration/models/providers/cloudflare.md: -------------------------------------------------------------------------------- 1 | # Cloudflare 2 | 3 | | Feature | Available | 4 | | --------------------------- | --------- | 5 | | [Tools](../tools) | No | 6 | | [Multimodal](../multimodal) | No | 7 | 8 | You may use Cloudflare Workers AI to run your own models with serverless inference. 9 | 10 | You will need to have a Cloudflare account, then get your [account ID](https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/) as well as your [API token](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-an-api-token) for Workers AI. 11 | 12 | You can either specify them directly in your `.env.local` using the `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` variables, or you can set them directly in the endpoint config. 13 | 14 | You can find the list of models available on Cloudflare [here](https://developers.cloudflare.com/workers-ai/models/#text-generation). 15 | 16 | ```ini 17 | MODELS=`[ 18 | { 19 | "name" : "nousresearch/hermes-2-pro-mistral-7b", 20 | "tokenizer": "nousresearch/hermes-2-pro-mistral-7b", 21 | "parameters": { 22 | "stop": ["<|im_end|>"] 23 | }, 24 | "endpoints" : [ 25 | { 26 | "type" : "cloudflare" 27 | 31 | } 32 | ] 33 | } 34 | ]` 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/source/configuration/models/providers/cohere.md: -------------------------------------------------------------------------------- 1 | # Cohere 2 | 3 | | Feature | Available | 4 | | --------------------------- | --------- | 5 | | [Tools](../tools) | Yes | 6 | | [Multimodal](../multimodal) | No | 7 | 8 | You may use Cohere to run their models directly from Chat UI. You will need to have a Cohere account, then get your [API token](https://dashboard.cohere.com/api-keys). You can either specify it directly in your `.env.local` using the `COHERE_API_TOKEN` variable, or you can set it in the endpoint config. 9 | 10 | Here is an example of a Cohere model config. You can set which model you want to use by setting the `id` field to the model name. 11 | 12 | ```ini 13 | MODELS=`[ 14 | { 15 | "name": "command-r-plus", 16 | "displayName": "Command R+", 17 | "tools": true, 18 | "endpoints": [{ 19 | "type": "cohere", 20 | 23 | }] 24 | } 25 | ]` 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/source/configuration/models/providers/langserve.md: -------------------------------------------------------------------------------- 1 | # LangServe 2 | 3 | | Feature | Available | 4 | | --------------------------- | --------- | 5 | | [Tools](../tools) | No | 6 | | [Multimodal](../multimodal) | No | 7 | 8 | LangChain applications that are deployed using LangServe can be called with the following config: 9 | 10 | ```ini 11 | MODELS=`[ 12 | { 13 | "name": "summarization-chain", 14 | "displayName": "Summarization Chain" 15 | "endpoints" : [{ 16 | "type": "langserve", 17 | "url" : "http://127.0.0.1:8100", 18 | }] 19 | } 20 | ]` 21 | 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/source/configuration/models/providers/ollama.md: -------------------------------------------------------------------------------- 1 | # Ollama 2 | 3 | | Feature | Available | 4 | | --------------------------- | --------- | 5 | | [Tools](../tools) | No | 6 | | [Multimodal](../multimodal) | No | 7 | 8 | We also support the Ollama inference server. Spin up a model with 9 | 10 | ```bash 11 | ollama run mistral 12 | ``` 13 | 14 | Then specify the endpoints like so: 15 | 16 | ```ini 17 | MODELS=`[ 18 | { 19 | "name": "Ollama Mistral", 20 | "chatPromptTemplate": "{{#each messages}}{{#ifUser}}[INST] {{#if @first}}{{#if @root.preprompt}}{{@root.preprompt}}\n{{/if}}{{/if}} {{content}} [/INST]{{/ifUser}}{{#ifAssistant}}{{content}} {{/ifAssistant}}{{/each}}", 21 | "parameters": { 22 | "temperature": 0.1, 23 | "top_p": 0.95, 24 | "repetition_penalty": 1.2, 25 | "top_k": 50, 26 | "truncate": 3072, 27 | "max_new_tokens": 1024, 28 | "stop": [""] 29 | }, 30 | "endpoints": [ 31 | { 32 | "type": "ollama", 33 | "url" : "http://127.0.0.1:11434", 34 | "ollamaName" : "mistral" 35 | } 36 | ] 37 | } 38 | ]` 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/source/configuration/open-id.md: -------------------------------------------------------------------------------- 1 | # OpenID 2 | 3 | The login feature is disabled by default and users are attributed a unique ID based on their browser. But if you want to use OpenID to authenticate your users, you can add the following to your `.env.local` file: 4 | 5 | ```ini 6 | OPENID_CONFIG=`{ 7 | PROVIDER_URL: "", 8 | CLIENT_ID: "", 9 | CLIENT_SECRET: "", 10 | SCOPES: "openid profile", 11 | TOLERANCE: // optional 12 | RESOURCE: // optional 13 | }` 14 | ``` 15 | 16 | Redirect URI: `/login/callback` 17 | -------------------------------------------------------------------------------- /docs/source/configuration/overview.md: -------------------------------------------------------------------------------- 1 | # Configuration Overview 2 | 3 | Chat UI handles configuration with environment variables. The default config for Chat UI is stored in the `.env` file, which you may use as a reference. You will need to override some values to get Chat UI to run locally. This can be done in `.env.local` or via your environment. The bare minimum configuration to get Chat UI running is: 4 | 5 | ```ini 6 | MONGODB_URL=mongodb://localhost:27017 7 | HF_TOKEN=your_token 8 | ``` 9 | 10 | The following sections detail various sections of the app you may want to configure. 11 | -------------------------------------------------------------------------------- /docs/source/configuration/theming.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | You can use a few environment variables to customize the look and feel of Chat UI. These are by default: 4 | 5 | ```ini 6 | PUBLIC_APP_NAME=ChatUI 7 | PUBLIC_APP_ASSETS=chatui 8 | PUBLIC_APP_COLOR=blue 9 | PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone." 10 | PUBLIC_APP_DATA_SHARING= 11 | PUBLIC_APP_DISCLAIMER= 12 | ``` 13 | 14 | - `PUBLIC_APP_NAME` The name used as a title throughout the app. 15 | - `PUBLIC_APP_ASSETS` Is used to find logos & favicons in `static/$PUBLIC_APP_ASSETS`, current options are `chatui` and `huggingchat`. 16 | - `PUBLIC_APP_COLOR` Can be any of the [tailwind colors](https://tailwindcss.com/docs/customizing-colors#default-color-palette). 17 | - `PUBLIC_APP_DATA_SHARING` Can be set to 1 to add a toggle in the user settings that lets your users opt-in to data sharing with models creator. 18 | - `PUBLIC_APP_DISCLAIMER` If set to 1, we show a disclaimer about generated outputs on login. 19 | -------------------------------------------------------------------------------- /docs/source/installation/docker.md: -------------------------------------------------------------------------------- 1 | # Running on Docker 2 | 3 | Pre-built docker images are provided with and without MongoDB built in. Refer to the [configuration section](../configuration/overview) for env variables that must be provided. We recommend using the `--env-file` option to avoid leaking secrets into your shell history. 4 | 5 | ```bash 6 | # Without built-in DB 7 | docker run -p 3000:3000 --env-file .env.local --name chat-ui ghcr.io/huggingface/chat-ui 8 | 9 | # With built-in DB 10 | docker run -p 3000:3000 --env-file .env.local -v chat-ui:/data --name chat-ui ghcr.io/huggingface/chat-ui-db 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/source/installation/helm.md: -------------------------------------------------------------------------------- 1 | # Helm 2 | 3 | 4 | 5 | **We highly discourage using the chart**. The Helm chart is a work in progress and should be considered unstable. Breaking changes to the chart may be pushed without migration guides or notice. Contributions welcome! 6 | 7 | 8 | 9 | For installation on Kubernetes, you may use the helm chart in `/chart`. Please note that no chart repository has been setup, so you'll need to clone the repository and install the chart by path. The production values may be found at `chart/env/prod.yaml`. 10 | 11 | **Example values.yaml** 12 | 13 | ```yaml 14 | replicas: 1 15 | 16 | domain: example.com 17 | 18 | service: 19 | type: ClusterIP 20 | 21 | resources: 22 | requests: 23 | cpu: 100m 24 | memory: 2Gi 25 | limits: 26 | # Recommended to use large limits when web search is enabled 27 | cpu: "4" 28 | memory: 6Gi 29 | 30 | envVars: 31 | MONGODB_URL: mongodb://chat-ui-mongo:27017 32 | # Ensure that your values.yaml will not leak anywhere 33 | # PRs welcome for a chart rework with envFrom support! 34 | HF_TOKEN: secret_token 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/source/installation/local.md: -------------------------------------------------------------------------------- 1 | # Running Locally 2 | 3 | You may start an instance locally for non-production use cases. For production use cases, please see the other installation options. 4 | 5 | ## Configuration 6 | 7 | The default config for Chat UI is stored in the `.env` file. You will need to override some values to get Chat UI to run locally. Start by creating a `.env.local` file in the root of the repository as per the [configuration section](../configuration/overview). The bare minimum config you need to get Chat UI to run locally is the following: 8 | 9 | ```ini 10 | MONGODB_URL= 11 | HF_TOKEN= # find your token at hf.co/settings/token 12 | ``` 13 | 14 | ## Database 15 | 16 | The chat history is stored in a MongoDB instance, and having a DB instance available is needed for Chat UI to work. 17 | 18 | You can use a local MongoDB instance. The easiest way is to spin one up using docker with persistence: 19 | 20 | ```bash 21 | docker run -d -p 27017:27017 -v mongo-chat-ui:/data --name mongo-chat-ui mongo:latest 22 | ``` 23 | 24 | In which case the url of your DB will be `MONGODB_URL=mongodb://localhost:27017`. 25 | 26 | Alternatively, you can use a [free MongoDB Atlas](https://www.mongodb.com/pricing) instance for this, Chat UI should fit comfortably within their free tier. After which you can set the `MONGODB_URL` variable in `.env.local` to match your instance. 27 | 28 | ## Starting the server 29 | 30 | ```bash 31 | npm ci # install dependencies 32 | npm run build # build the project 33 | npm run preview -- --open # start the server with & open your instance at http://localhost:4173 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/source/installation/spaces.md: -------------------------------------------------------------------------------- 1 | # Running on Huggingface Spaces 2 | 3 | If you don't want to configure, setup, and launch your own Chat UI yourself, you can use this option as a fast deploy alternative. 4 | 5 | You can deploy your own customized Chat UI instance with any supported [LLM](https://huggingface.co/models?pipeline_tag=text-generation) of your choice on [Hugging Face Spaces](https://huggingface.co/spaces). To do so, use the chat-ui template [available here](https://huggingface.co/new-space?template=huggingchat/chat-ui-template). 6 | 7 | Set `HF_TOKEN` in [Space secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables) to deploy a model with gated access or a model in a private repository. It's also compatible with [Inference for PROs](https://huggingface.co/blog/inference-pro) curated list of powerful models with higher rate limits. Make sure to create your personal token first in your [User Access Tokens settings](https://huggingface.co/settings/tokens). 8 | 9 | Read the full tutorial [here](https://huggingface.co/docs/hub/spaces-sdks-docker-chatui#chatui-on-spaces). 10 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | ENV_LOCAL_PATH=/app/.env.local 2 | 3 | if test -z "${DOTENV_LOCAL}" ; then 4 | if ! test -f "${ENV_LOCAL_PATH}" ; then 5 | echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. Make sure to set environment variables properly. " 6 | fi; 7 | else 8 | echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file." 9 | cat <<< "$DOTENV_LOCAL" > ${ENV_LOCAL_PATH} 10 | fi; 11 | 12 | if [ "$INCLUDE_DB" = "true" ] ; then 13 | echo "Starting local MongoDB instance" 14 | nohup mongod & 15 | fi; 16 | 17 | export PUBLIC_VERSION=$(node -p "require('./package.json').version") 18 | 19 | dotenv -e /app/.env -c -- node /app/build/index.js -- --host 0.0.0.0 --port 3000 -------------------------------------------------------------------------------- /models/add-your-models-here.txt: -------------------------------------------------------------------------------- 1 | You can add .gguf files to this folder, and they will be picked up automatically by chat-ui. -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/config.ts: -------------------------------------------------------------------------------- 1 | import sade from "sade"; 2 | 3 | // @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them 4 | import { config, ready } from "$lib/server/config"; 5 | 6 | const prog = sade("config"); 7 | await ready; 8 | prog 9 | .command("clear") 10 | .describe("Clear all config keys") 11 | .action(async () => { 12 | console.log("Clearing config..."); 13 | await clear(); 14 | }); 15 | 16 | prog 17 | .command("add ") 18 | .describe("Add a new config key") 19 | .action(async (key: string, value: string) => { 20 | await add(key, value); 21 | }); 22 | 23 | prog 24 | .command("remove ") 25 | .describe("Remove a config key") 26 | .action(async (key: string) => { 27 | console.log(`Removing ${key}`); 28 | await remove(key); 29 | process.exit(0); 30 | }); 31 | 32 | prog 33 | .command("help") 34 | .describe("Show help information") 35 | .action(() => { 36 | prog.help(); 37 | process.exit(0); 38 | }); 39 | 40 | async function clear() { 41 | await config.clear(); 42 | process.exit(0); 43 | } 44 | 45 | async function add(key: string, value: string) { 46 | if (!key || !value) { 47 | console.error("Key and value are required"); 48 | process.exit(1); 49 | } 50 | await config.set(key as keyof typeof config.keysFromEnv, value); 51 | process.exit(0); 52 | } 53 | 54 | async function remove(key: string) { 55 | if (!key) { 56 | console.error("Key is required"); 57 | process.exit(1); 58 | } 59 | await config.delete(key as keyof typeof config.keysFromEnv); 60 | process.exit(0); 61 | } 62 | 63 | // Parse arguments and handle help automatically 64 | prog.parse(process.argv); 65 | -------------------------------------------------------------------------------- /scripts/setups/vitest-setup-client.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/scripts/setups/vitest-setup-client.ts -------------------------------------------------------------------------------- /scripts/setups/vitest-setup-server.ts: -------------------------------------------------------------------------------- 1 | import { vi, afterAll } from "vitest"; 2 | import dotenv from "dotenv"; 3 | import { resolve } from "path"; 4 | import fs from "fs"; 5 | import { MongoMemoryServer } from "mongodb-memory-server"; 6 | 7 | let mongoServer: MongoMemoryServer; 8 | // Load the .env file 9 | const envPath = resolve(__dirname, "../../.env"); 10 | dotenv.config({ path: envPath }); 11 | 12 | // Read the .env file content 13 | const envContent = fs.readFileSync(envPath, "utf-8"); 14 | 15 | // Parse the .env content 16 | const envVars = dotenv.parse(envContent); 17 | 18 | // Separate public and private variables 19 | const publicEnv = {}; 20 | const privateEnv = {}; 21 | 22 | for (const [key, value] of Object.entries(envVars)) { 23 | if (key.startsWith("PUBLIC_")) { 24 | publicEnv[key] = value; 25 | } else { 26 | privateEnv[key] = value; 27 | } 28 | } 29 | 30 | vi.mock("$env/dynamic/public", () => ({ 31 | env: publicEnv, 32 | })); 33 | 34 | vi.mock("$env/dynamic/private", async () => { 35 | mongoServer = await MongoMemoryServer.create(); 36 | 37 | return { 38 | env: { 39 | ...privateEnv, 40 | MONGODB_URL: mongoServer.getUri(), 41 | }, 42 | }; 43 | }); 44 | 45 | afterAll(async () => { 46 | if (mongoServer) { 47 | await mongoServer.stop(); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /scripts/updateLocalEnv.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import yaml from "js-yaml"; 3 | 4 | const file = fs.readFileSync("chart/env/prod.yaml", "utf8"); 5 | 6 | // have to do a weird stringify/parse because of some node error 7 | const prod = JSON.parse(JSON.stringify(yaml.load(file))); 8 | const vars = prod.envVars as Record; 9 | 10 | let PUBLIC_CONFIG = ""; 11 | 12 | Object.entries(vars) 13 | // filter keys used in prod with the proxy 14 | .filter( 15 | ([key]) => 16 | ![ 17 | "XFF_DEPTH", 18 | "ADDRESS_HEADER", 19 | "APP_BASE", 20 | "PUBLIC_ORIGIN", 21 | "PUBLIC_SHARE_PREFIX", 22 | "ADMIN_CLI_LOGIN", 23 | ].includes(key) 24 | ) 25 | .forEach(([key, value]) => { 26 | PUBLIC_CONFIG += `${key}=\`${value}\`\n`; 27 | }); 28 | 29 | const SECRET_CONFIG = 30 | (fs.existsSync(".env.SECRET_CONFIG") 31 | ? fs.readFileSync(".env.SECRET_CONFIG", "utf8") 32 | : process.env.SECRET_CONFIG) ?? ""; 33 | 34 | // Prepend the content of the env variable SECRET_CONFIG 35 | let full_config = `${PUBLIC_CONFIG}\n${SECRET_CONFIG}`; 36 | 37 | // replace the internal proxy url with the public endpoint 38 | full_config = full_config.replaceAll( 39 | "https://internal.api-inference.huggingface.co", 40 | "https://router.huggingface.co/hf-inference" 41 | ); 42 | 43 | full_config = full_config.replaceAll("COOKIE_SECURE=`true`", "COOKIE_SECURE=`false`"); 44 | full_config = full_config.replaceAll("LOG_LEVEL=`debug`", "LOG_LEVEL=`info`"); 45 | full_config = full_config.replaceAll("NODE_ENV=`prod`", "NODE_ENV=`development`"); 46 | 47 | // Write full_config to .env.local 48 | fs.writeFileSync(".env.local", full_config); 49 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.ttf" { 2 | const value: ArrayBuffer; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import type { User } from "$lib/types/User"; 5 | 6 | // See https://kit.svelte.dev/docs/types#app 7 | // for information about these interfaces 8 | declare global { 9 | namespace App { 10 | // interface Error {} 11 | interface Locals { 12 | sessionId: string; 13 | user?: User & { logoutDisabled?: boolean }; 14 | isAdmin: boolean; 15 | } 16 | 17 | interface Error { 18 | message: string; 19 | errorId?: ReturnType; 20 | } 21 | // interface PageData {} 22 | // interface Platform {} 23 | } 24 | } 25 | 26 | export {}; 27 | -------------------------------------------------------------------------------- /src/lib/actions/clickOutside.ts: -------------------------------------------------------------------------------- 1 | export function clickOutside(element: HTMLElement, callbackFunction: () => void) { 2 | function onClick(event: MouseEvent) { 3 | if (!element.contains(event.target as Node)) { 4 | callbackFunction(); 5 | } 6 | } 7 | 8 | document.body.addEventListener("click", onClick); 9 | 10 | return { 11 | update(newCallbackFunction: () => void) { 12 | callbackFunction = newCallbackFunction; 13 | }, 14 | destroy() { 15 | document.body.removeEventListener("click", onClick); 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/actions/snapScrollToBottom.ts: -------------------------------------------------------------------------------- 1 | import { navigating } from "$app/stores"; 2 | import { tick } from "svelte"; 3 | import { get } from "svelte/store"; 4 | 5 | const detachedOffset = 10; 6 | 7 | /** 8 | * @param node element to snap scroll to bottom 9 | * @param dependency pass in a dependency to update scroll on changes. 10 | */ 11 | export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => { 12 | let prevScrollValue = node.scrollTop; 13 | let isDetached = false; 14 | 15 | const handleScroll = () => { 16 | // if user scrolled up, we detach 17 | if (node.scrollTop < prevScrollValue) { 18 | isDetached = true; 19 | } 20 | 21 | // if user scrolled back to within 10px of bottom, we reattach 22 | if (node.scrollTop - (node.scrollHeight - node.clientHeight) >= -detachedOffset) { 23 | isDetached = false; 24 | } 25 | 26 | prevScrollValue = node.scrollTop; 27 | }; 28 | 29 | const updateScroll = async (_options: { force?: boolean } = {}) => { 30 | const defaultOptions = { force: false }; 31 | const options = { ...defaultOptions, ..._options }; 32 | const { force } = options; 33 | 34 | if (!force && isDetached && !get(navigating)) return; 35 | 36 | // wait for next tick to ensure that the DOM is updated 37 | await tick(); 38 | 39 | node.scrollTo({ top: node.scrollHeight }); 40 | }; 41 | 42 | node.addEventListener("scroll", handleScroll); 43 | 44 | if (dependency) { 45 | updateScroll({ force: true }); 46 | } 47 | 48 | return { 49 | update: updateScroll, 50 | destroy: () => { 51 | node.removeEventListener("scroll", handleScroll); 52 | }, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/lib/components/AnnouncementBanner.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | New 16 | {title} 17 |
18 | {@render children?.()} 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/CodeBlock.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
{@html DOMPurify.sanitize(code)}
18 | 22 |
23 | -------------------------------------------------------------------------------- /src/lib/components/ContinueBtn.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ExpandNavigation.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/lib/components/HoverTooltip.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {@render children?.()} 21 | 22 | 44 |
45 | -------------------------------------------------------------------------------- /src/lib/components/InfiniteScroll.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /src/lib/components/PaginationArrow.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | {#if direction === "previous"} 21 | 22 | Previous 23 | {:else} 24 | Next 25 | 26 | {/if} 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/Portal.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/lib/components/RetryBtn.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ScrollToBottomBtn.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | {#if visible} 41 | 47 | {/if} 48 | -------------------------------------------------------------------------------- /src/lib/components/StopGeneratingBtn.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | -------------------------------------------------------------------------------- /src/lib/components/Switch.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/SystemPromptModal.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 24 | {#if isOpen} 25 | (isOpen = false)} width="w-full !max-w-xl"> 26 |
27 |
28 |

System Prompt

29 | 32 |
33 | 38 |
39 |
40 | {/if} 41 | -------------------------------------------------------------------------------- /src/lib/components/Toast.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 |
18 |
21 | 22 |

{message}

23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/TokensCounter.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
34 |

37 | {nTokens}{truncate ? `/${truncate}` : ""} 38 |

39 | 44 |
45 | -------------------------------------------------------------------------------- /src/lib/components/ToolBadge.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
16 | {#if browser} 17 | {#await fetch(`${base}/api/tools/${toolId}`).then((res) => res.json()) then value} 18 | {#key value.color + value.icon} 19 | 20 | {/key} 21 |
22 | {value.displayName} 28 | {#if value.createdByName} 29 |

30 | Created by 31 | {value.createdByName} 34 |

35 | {:else} 36 |

Official HuggingChat tool

37 | {/if} 38 |
39 | {/await} 40 | {/if} 41 |
42 | -------------------------------------------------------------------------------- /src/lib/components/Tooltip.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
22 | 29 | {label} 30 |
31 | -------------------------------------------------------------------------------- /src/lib/components/UploadBtn.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /src/lib/components/WebSearchToggle.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
18 | 19 | 22 |
23 | 24 |
27 |

28 | When enabled, the model will try to complement its answer with information queried from the 29 | web. 30 |

31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/lib/components/chat/Vote.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 41 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconChevron.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconCopy.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconInternet.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconLoading.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
14 |
18 |
22 |
23 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconNew.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconPaperclip.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /src/lib/components/icons/IconScreenshot.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /src/lib/components/icons/Logo.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 22 | 23 | 24 | {#if publicConfig.PUBLIC_APP_ASSETS === "chatui"} 25 | 32 | 36 | 37 | {:else} 38 | {publicConfig.PUBLIC_APP_NAME} logo 44 | {/if} 45 | -------------------------------------------------------------------------------- /src/lib/constants/pagination.ts: -------------------------------------------------------------------------------- 1 | export const CONV_NUM_PER_PAGE = 30; 2 | -------------------------------------------------------------------------------- /src/lib/constants/publicSepToken.ts: -------------------------------------------------------------------------------- 1 | export const PUBLIC_SEP_TOKEN = ""; 2 | -------------------------------------------------------------------------------- /src/lib/migrations/lock.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "$lib/server/database"; 2 | import { ObjectId } from "mongodb"; 3 | import type { Semaphores } from "$lib/types/Semaphore"; 4 | 5 | /** 6 | * Returns the lock id if the lock was acquired, false otherwise 7 | */ 8 | export async function acquireLock(key: Semaphores): Promise { 9 | try { 10 | const id = new ObjectId(); 11 | 12 | const insert = await collections.semaphores.insertOne({ 13 | _id: id, 14 | key, 15 | createdAt: new Date(), 16 | updatedAt: new Date(), 17 | deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes 18 | }); 19 | 20 | return insert.acknowledged ? id : false; // true if the document was inserted 21 | } catch (e) { 22 | // unique index violation, so there must already be a lock 23 | return false; 24 | } 25 | } 26 | 27 | export async function releaseLock(key: Semaphores, lockId: ObjectId) { 28 | await collections.semaphores.deleteOne({ 29 | _id: lockId, 30 | key, 31 | }); 32 | } 33 | 34 | export async function isDBLocked(key: Semaphores): Promise { 35 | const res = await collections.semaphores.countDocuments({ 36 | key, 37 | }); 38 | return res > 0; 39 | } 40 | 41 | export async function refreshLock(key: Semaphores, lockId: ObjectId): Promise { 42 | const result = await collections.semaphores.updateOne( 43 | { 44 | _id: lockId, 45 | key, 46 | }, 47 | { 48 | $set: { 49 | updatedAt: new Date(), 50 | deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes 51 | }, 52 | } 53 | ); 54 | 55 | return result.matchedCount > 0; 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/01-update-search-assistants.ts: -------------------------------------------------------------------------------- 1 | import type { Migration } from "."; 2 | import { collections } from "$lib/server/database"; 3 | import { ObjectId, type AnyBulkWriteOperation } from "mongodb"; 4 | import type { Assistant } from "$lib/types/Assistant"; 5 | import { generateSearchTokens } from "$lib/utils/searchTokens"; 6 | 7 | const migration: Migration = { 8 | _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"), 9 | name: "Update search assistants", 10 | up: async () => { 11 | const { assistants } = collections; 12 | let ops: AnyBulkWriteOperation[] = []; 13 | 14 | for await (const assistant of assistants 15 | .find() 16 | .project>({ _id: 1, name: 1 })) { 17 | ops.push({ 18 | updateOne: { 19 | filter: { 20 | _id: assistant._id, 21 | }, 22 | update: { 23 | $set: { 24 | searchTokens: generateSearchTokens(assistant.name), 25 | }, 26 | }, 27 | }, 28 | }); 29 | 30 | if (ops.length >= 1000) { 31 | process.stdout.write("."); 32 | await assistants.bulkWrite(ops, { ordered: false }); 33 | ops = []; 34 | } 35 | } 36 | 37 | if (ops.length) { 38 | await assistants.bulkWrite(ops, { ordered: false }); 39 | } 40 | 41 | return true; 42 | }, 43 | down: async () => { 44 | const { assistants } = collections; 45 | await assistants.updateMany({}, { $unset: { searchTokens: "" } }); 46 | return true; 47 | }, 48 | }; 49 | 50 | export default migration; 51 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/02-update-assistants-models.ts: -------------------------------------------------------------------------------- 1 | import type { Migration } from "."; 2 | import { collections } from "$lib/server/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const updateAssistantsModels: Migration = { 6 | _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"), 7 | name: "Update deprecated models in assistants with the default model", 8 | up: async () => { 9 | const models = (await import("$lib/server/models")).models; 10 | const oldModels = (await import("$lib/server/models")).oldModels; 11 | const { assistants } = collections; 12 | 13 | const modelIds = models.map((el) => el.id); 14 | const defaultModelId = models[0].id; 15 | 16 | // Find all assistants whose modelId is not in modelIds, and update it 17 | const bulkOps = await assistants 18 | .find({ modelId: { $nin: modelIds } }) 19 | .map((assistant) => { 20 | // has an old model 21 | let newModelId = defaultModelId; 22 | 23 | const oldModel = oldModels.find((m) => m.id === assistant.modelId); 24 | if (oldModel && oldModel.transferTo && !!models.find((m) => m.id === oldModel.transferTo)) { 25 | newModelId = oldModel.transferTo; 26 | } 27 | 28 | return { 29 | updateOne: { 30 | filter: { _id: assistant._id }, 31 | update: { $set: { modelId: newModelId } }, 32 | }, 33 | }; 34 | }) 35 | .toArray(); 36 | 37 | if (bulkOps.length > 0) { 38 | await assistants.bulkWrite(bulkOps); 39 | } 40 | 41 | return true; 42 | }, 43 | runEveryTime: true, 44 | runForHuggingChat: "only", 45 | }; 46 | 47 | export default updateAssistantsModels; 48 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/03-add-tools-in-settings.ts: -------------------------------------------------------------------------------- 1 | import type { Migration } from "."; 2 | import { collections } from "$lib/server/database"; 3 | import { ObjectId } from "mongodb"; 4 | import { logger } from "$lib/server/logger"; 5 | 6 | const addToolsToSettings: Migration = { 7 | _id: new ObjectId("5c9c4c4c4c4c4c4c4c4c4c4c"), 8 | name: "Add empty 'tools' record in settings", 9 | up: async () => { 10 | const { settings } = collections; 11 | 12 | // Find all assistants whose modelId is not in modelIds, and update it to use defaultModelId 13 | await settings.updateMany( 14 | { 15 | tools: { $exists: false }, 16 | }, 17 | { $set: { tools: [] } } 18 | ); 19 | 20 | settings 21 | .createIndex({ tools: 1 }) 22 | .catch((e) => logger.error(e, "Error creating index during tools migration")); 23 | 24 | return true; 25 | }, 26 | runEveryTime: false, 27 | }; 28 | 29 | export default addToolsToSettings; 30 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/07-reset-tools-in-settings.ts: -------------------------------------------------------------------------------- 1 | import type { Migration } from "."; 2 | import { collections } from "$lib/server/database"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | const resetTools: Migration = { 6 | _id: new ObjectId("000000000000000000000007"), 7 | name: "Reset tools to empty", 8 | up: async () => { 9 | const { settings } = collections; 10 | 11 | await settings.updateMany({}, { $set: { tools: [] } }); 12 | 13 | return true; 14 | }, 15 | runEveryTime: false, 16 | }; 17 | 18 | export default resetTools; 19 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/08-update-featured-to-review.ts: -------------------------------------------------------------------------------- 1 | import type { Migration } from "."; 2 | import { collections } from "$lib/server/database"; 3 | import { ObjectId } from "mongodb"; 4 | import { ReviewStatus } from "$lib/types/Review"; 5 | 6 | const updateFeaturedToReview: Migration = { 7 | _id: new ObjectId("000000000000000000000008"), 8 | name: "Update featured to review", 9 | up: async () => { 10 | const { assistants, tools } = collections; 11 | 12 | // Update assistants 13 | await assistants.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); 14 | await assistants.updateMany( 15 | { featured: { $ne: true } }, 16 | { $set: { review: ReviewStatus.PRIVATE } } 17 | ); 18 | 19 | await assistants.updateMany({}, { $unset: { featured: "" } }); 20 | 21 | // Update tools 22 | await tools.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); 23 | await tools.updateMany({ featured: { $ne: true } }, { $set: { review: ReviewStatus.PRIVATE } }); 24 | 25 | await tools.updateMany({}, { $unset: { featured: "" } }); 26 | 27 | return true; 28 | }, 29 | runEveryTime: false, 30 | }; 31 | 32 | export default updateFeaturedToReview; 33 | -------------------------------------------------------------------------------- /src/lib/migrations/routines/index.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | 3 | import updateSearchAssistant from "./01-update-search-assistants"; 4 | import updateAssistantsModels from "./02-update-assistants-models"; 5 | import type { Database } from "$lib/server/database"; 6 | import addToolsToSettings from "./03-add-tools-in-settings"; 7 | import updateMessageUpdates from "./04-update-message-updates"; 8 | import updateMessageFiles from "./05-update-message-files"; 9 | import trimMessageUpdates from "./06-trim-message-updates"; 10 | import resetTools from "./07-reset-tools-in-settings"; 11 | import updateFeaturedToReview from "./08-update-featured-to-review"; 12 | import deleteEmptyConversations from "./09-delete-empty-conversations"; 13 | export interface Migration { 14 | _id: ObjectId; 15 | name: string; 16 | up: (client: Database) => Promise; 17 | down?: (client: Database) => Promise; 18 | runForFreshInstall?: "only" | "never"; // leave unspecified to run for both 19 | runForHuggingChat?: "only" | "never"; // leave unspecified to run for both 20 | runEveryTime?: boolean; 21 | } 22 | 23 | export const migrations: Migration[] = [ 24 | updateSearchAssistant, 25 | updateAssistantsModels, 26 | addToolsToSettings, 27 | updateMessageUpdates, 28 | updateMessageFiles, 29 | trimMessageUpdates, 30 | resetTools, 31 | updateFeaturedToReview, 32 | deleteEmptyConversations, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/lib/server/abortedGenerations.ts: -------------------------------------------------------------------------------- 1 | // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 2 | 3 | import { logger } from "$lib/server/logger"; 4 | import { collections } from "$lib/server/database"; 5 | import { onExit } from "./exitHandler"; 6 | 7 | export class AbortedGenerations { 8 | private static instance: AbortedGenerations; 9 | 10 | private abortedGenerations: Record = {}; 11 | 12 | private constructor() { 13 | const interval = setInterval(() => this.updateList(), 1000); 14 | onExit(() => clearInterval(interval)); 15 | 16 | this.updateList(); 17 | } 18 | 19 | public static getInstance(): AbortedGenerations { 20 | if (!AbortedGenerations.instance) { 21 | AbortedGenerations.instance = new AbortedGenerations(); 22 | } 23 | 24 | return AbortedGenerations.instance; 25 | } 26 | 27 | public getAbortTime(conversationId: string): Date | undefined { 28 | return this.abortedGenerations[conversationId]; 29 | } 30 | 31 | private async updateList() { 32 | try { 33 | const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); 34 | 35 | this.abortedGenerations = Object.fromEntries( 36 | aborts.map((abort) => [abort.conversationId.toString(), abort.createdAt]) 37 | ); 38 | } catch (err) { 39 | logger.error(err); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts: -------------------------------------------------------------------------------- 1 | import type { TextGenerationStreamOutput } from "@huggingface/inference"; 2 | import type OpenAI from "openai"; 3 | import type { Stream } from "openai/streaming"; 4 | 5 | /** 6 | * Transform a stream of OpenAI.Completions.Completion into a stream of TextGenerationStreamOutput 7 | */ 8 | export async function* openAICompletionToTextGenerationStream( 9 | completionStream: Stream 10 | ) { 11 | let generatedText = ""; 12 | let tokenId = 0; 13 | for await (const completion of completionStream) { 14 | const { choices } = completion; 15 | const text = choices[0]?.text ?? ""; 16 | const last = choices[0]?.finish_reason === "stop" || choices[0]?.finish_reason === "length"; 17 | if (text) { 18 | generatedText = generatedText + text; 19 | } 20 | const output: TextGenerationStreamOutput = { 21 | token: { 22 | id: tokenId++, 23 | text, 24 | logprob: 0, 25 | special: last, 26 | }, 27 | generated_text: last ? generatedText : null, 28 | details: null, 29 | }; 30 | yield output; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/server/files/downloadFile.ts: -------------------------------------------------------------------------------- 1 | import { error } from "@sveltejs/kit"; 2 | import { collections } from "$lib/server/database"; 3 | import type { Conversation } from "$lib/types/Conversation"; 4 | import type { SharedConversation } from "$lib/types/SharedConversation"; 5 | import type { MessageFile } from "$lib/types/Message"; 6 | 7 | export async function downloadFile( 8 | sha256: string, 9 | convId: Conversation["_id"] | SharedConversation["_id"] 10 | ): Promise { 11 | const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); 12 | 13 | const file = await fileId.next(); 14 | if (!file) { 15 | error(404, "File not found"); 16 | } 17 | if (file.metadata?.conversation !== convId.toString()) { 18 | error(403, "You don't have access to this file."); 19 | } 20 | 21 | const mime = file.metadata?.mime; 22 | const name = file.filename; 23 | 24 | const fileStream = collections.bucket.openDownloadStream(file._id); 25 | 26 | const buffer = await new Promise((resolve, reject) => { 27 | const chunks: Uint8Array[] = []; 28 | fileStream.on("data", (chunk) => chunks.push(chunk)); 29 | fileStream.on("error", reject); 30 | fileStream.on("end", () => resolve(Buffer.concat(chunks))); 31 | }); 32 | 33 | return { type: "base64", name, value: buffer.toString("base64"), mime }; 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/server/files/uploadFile.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import type { MessageFile } from "$lib/types/Message"; 3 | import { sha256 } from "$lib/utils/sha256"; 4 | import { fileTypeFromBuffer } from "file-type"; 5 | import { collections } from "$lib/server/database"; 6 | 7 | export async function uploadFile(file: File, conv: Conversation): Promise { 8 | const sha = await sha256(await file.text()); 9 | const buffer = await file.arrayBuffer(); 10 | 11 | // Attempt to detect the mime type of the file, fallback to the uploaded mime 12 | const mime = await fileTypeFromBuffer(buffer).then((fileType) => fileType?.mime ?? file.type); 13 | 14 | const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, { 15 | metadata: { conversation: conv._id.toString(), mime }, 16 | }); 17 | 18 | upload.write((await file.arrayBuffer()) as unknown as Buffer); 19 | upload.end(); 20 | 21 | // only return the filename when upload throws a finish event or a 20s time out occurs 22 | return new Promise((resolve, reject) => { 23 | upload.once("finish", () => 24 | resolve({ type: "hash", value: sha, mime: file.type, name: file.name }) 25 | ); 26 | upload.once("error", reject); 27 | setTimeout(() => reject(new Error("Upload timed out")), 20_000); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/server/findRepoRoot.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join, dirname } from "path"; 3 | 4 | export function findRepoRoot(startPath: string): string { 5 | let currentPath = startPath; 6 | while (currentPath !== "/") { 7 | if (existsSync(join(currentPath, "package.json"))) { 8 | return currentPath; 9 | } 10 | currentPath = dirname(currentPath); 11 | } 12 | throw new Error("Could not find repository root (no package.json found)"); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Black.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-ExtraBold.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-ExtraLight.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Light.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /src/lib/server/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/lib/server/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /src/lib/server/generateFromDefaultEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { taskModel } from "$lib/server/models"; 2 | import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; 3 | import type { EndpointMessage } from "./endpoints/endpoints"; 4 | 5 | export async function* generateFromDefaultEndpoint({ 6 | messages, 7 | preprompt, 8 | generateSettings, 9 | }: { 10 | messages: EndpointMessage[]; 11 | preprompt?: string; 12 | generateSettings?: Record; 13 | }): AsyncGenerator { 14 | const endpoint = await taskModel.getEndpoint(); 15 | 16 | const tokenStream = await endpoint({ messages, preprompt, generateSettings }); 17 | 18 | try { 19 | for await (const output of tokenStream) { 20 | // if not generated_text is here it means the generation is not done 21 | if (output.generated_text) { 22 | let generated_text = output.generated_text; 23 | for (const stop of [...(taskModel.parameters?.stop ?? []), "<|endoftext|>"]) { 24 | if (generated_text.endsWith(stop)) { 25 | generated_text = generated_text.slice(0, -stop.length).trimEnd(); 26 | } 27 | } 28 | return generated_text; 29 | } 30 | yield { 31 | type: MessageUpdateType.Stream, 32 | token: output.token.text, 33 | }; 34 | } 35 | } catch (error) { 36 | return ""; 37 | } 38 | 39 | return ""; 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/server/isURLLocal.spec.ts: -------------------------------------------------------------------------------- 1 | import { isURLLocal } from "./isURLLocal"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | describe("isURLLocal", async () => { 5 | it("should return true for localhost", async () => { 6 | expect(await isURLLocal(new URL("http://localhost"))).toBe(true); 7 | }); 8 | it("should return true for 127.0.0.1", async () => { 9 | expect(await isURLLocal(new URL("http://127.0.0.1"))).toBe(true); 10 | }); 11 | it("should return true for 127.254.254.254", async () => { 12 | expect(await isURLLocal(new URL("http://127.254.254.254"))).toBe(true); 13 | }); 14 | it("should return false for huggingface.co", async () => { 15 | expect(await isURLLocal(new URL("https://huggingface.co/"))).toBe(false); 16 | }); 17 | it("should return true for 127.0.0.1.nip.io", async () => { 18 | expect(await isURLLocal(new URL("http://127.0.0.1.nip.io"))).toBe(true); 19 | }); 20 | it("should fail on ipv6", async () => { 21 | await expect(isURLLocal(new URL("http://[::1]"))).rejects.toThrow(); 22 | }); 23 | it("should fail on ipv6 --1.sslip.io", async () => { 24 | await expect(isURLLocal(new URL("http://--1.sslip.io"))).rejects.toThrow(); 25 | }); 26 | it("should fail on invalid domain names", async () => { 27 | await expect( 28 | isURLLocal(new URL("http://34329487239847329874923948732984.com/")) 29 | ).rejects.toThrow(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/server/isURLLocal.ts: -------------------------------------------------------------------------------- 1 | import { Address6, Address4 } from "ip-address"; 2 | import dns from "node:dns"; 3 | 4 | const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => { 5 | return new Promise((resolve, reject) => { 6 | dns.lookup(hostname, (err, address, family) => { 7 | if (err) return reject(err); 8 | resolve({ address, family }); 9 | }); 10 | }); 11 | }; 12 | 13 | export async function isURLLocal(URL: URL): Promise { 14 | const { address, family } = await dnsLookup(URL.hostname); 15 | 16 | if (family === 4) { 17 | const addr = new Address4(address); 18 | const localSubnet = new Address4("127.0.0.0/8"); 19 | return addr.isInSubnet(localSubnet); 20 | } 21 | 22 | if (family === 6) { 23 | const addr = new Address6(address); 24 | return addr.isLoopback() || addr.isInSubnet(new Address6("::1/128")) || addr.isLinkLocal(); 25 | } 26 | 27 | throw Error("Unknown IP family"); 28 | } 29 | 30 | export function isURLStringLocal(url: string) { 31 | try { 32 | const urlObj = new URL(url); 33 | return isURLLocal(urlObj); 34 | } catch (e) { 35 | // assume local if URL parsing fails 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/server/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | import { dev } from "$app/environment"; 3 | import { config } from "$lib/server/config"; 4 | 5 | let options: pino.LoggerOptions = {}; 6 | 7 | if (dev) { 8 | options = { 9 | transport: { 10 | target: "pino-pretty", 11 | options: { 12 | colorize: true, 13 | }, 14 | }, 15 | }; 16 | } 17 | 18 | export const logger = pino({ ...options, level: config.LOG_LEVEL || "info" }); 19 | -------------------------------------------------------------------------------- /src/lib/server/sendSlack.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { logger } from "$lib/server/logger"; 3 | 4 | export async function sendSlack(text: string) { 5 | if (!config.WEBHOOK_URL_REPORT_ASSISTANT) { 6 | logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message."); 7 | return; 8 | } 9 | 10 | const res = await fetch(config.WEBHOOK_URL_REPORT_ASSISTANT, { 11 | method: "POST", 12 | headers: { 13 | "Content-type": "application/json", 14 | }, 15 | body: JSON.stringify({ 16 | text, 17 | }), 18 | }); 19 | 20 | if (!res.ok) { 21 | logger.error(`Webhook message failed. ${res.statusText} ${res.text}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/server/sentenceSimilarity.ts: -------------------------------------------------------------------------------- 1 | import { dot } from "@huggingface/transformers"; 2 | import type { EmbeddingBackendModel } from "$lib/server/embeddingModels"; 3 | import type { Embedding } from "$lib/server/embeddingEndpoints/embeddingEndpoints"; 4 | 5 | // see here: https://github.com/nmslib/hnswlib/blob/359b2ba87358224963986f709e593d799064ace6/README.md?plain=1#L34 6 | export function innerProduct(embeddingA: Embedding, embeddingB: Embedding) { 7 | return 1.0 - dot(embeddingA, embeddingB); 8 | } 9 | 10 | export async function getSentenceSimilarity( 11 | embeddingModel: EmbeddingBackendModel, 12 | query: string, 13 | sentences: string[] 14 | ): Promise<{ distance: number; embedding: Embedding; idx: number }[]> { 15 | const inputs = [ 16 | `${embeddingModel.preQuery}${query}`, 17 | ...sentences.map((sentence) => `${embeddingModel.prePassage}${sentence}`), 18 | ]; 19 | 20 | const embeddingEndpoint = await embeddingModel.getEndpoint(); 21 | const output = await embeddingEndpoint({ inputs }).catch((err) => { 22 | throw Error("Failed to generate embeddings for sentence similarity", { cause: err }); 23 | }); 24 | 25 | const queryEmbedding: Embedding = output[0]; 26 | const sentencesEmbeddings: Embedding[] = output.slice(1); 27 | 28 | return sentencesEmbeddings.map((sentenceEmbedding, idx) => ({ 29 | distance: innerProduct(queryEmbedding, sentenceEmbedding), 30 | embedding: sentenceEmbedding, 31 | idx, 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/server/textGeneration/types.ts: -------------------------------------------------------------------------------- 1 | import type { ProcessedModel } from "../models"; 2 | import type { Endpoint } from "../endpoints/endpoints"; 3 | import type { Conversation } from "$lib/types/Conversation"; 4 | import type { Message } from "$lib/types/Message"; 5 | import type { Assistant } from "$lib/types/Assistant"; 6 | 7 | export interface TextGenerationContext { 8 | model: ProcessedModel; 9 | endpoint: Endpoint; 10 | conv: Conversation; 11 | messages: Message[]; 12 | assistant?: Pick; 13 | isContinue: boolean; 14 | webSearch: boolean; 15 | toolsPreference: Array; 16 | promptedAt: Date; 17 | ip: string; 18 | username?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/server/tools/calculator.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigTool } from "$lib/types/Tool"; 2 | import { ObjectId } from "mongodb"; 3 | import vm from "node:vm"; 4 | 5 | const calculator: ConfigTool = { 6 | _id: new ObjectId("00000000000000000000000C"), 7 | type: "config", 8 | description: "Calculate the result of a mathematical expression", 9 | color: "blue", 10 | icon: "code", 11 | displayName: "Calculator", 12 | name: "calculator", 13 | endpoint: null, 14 | inputs: [ 15 | { 16 | name: "equation", 17 | type: "str", 18 | description: 19 | "A mathematical expression to be evaluated. The result of the expression will be returned.", 20 | paramType: "required", 21 | }, 22 | ], 23 | outputComponent: null, 24 | outputComponentIdx: null, 25 | showOutput: false, 26 | async *call({ equation }) { 27 | try { 28 | const blocks = String(equation).split("\n"); 29 | const query = blocks[blocks.length - 1].replace(/[^-()\d/*+.]/g, ""); 30 | 31 | return { 32 | outputs: [{ calculator: `${query} = ${vm.runInNewContext(query)}` }], 33 | }; 34 | } catch (cause) { 35 | throw new Error("Invalid expression", { cause }); 36 | } 37 | }, 38 | }; 39 | 40 | export default calculator; 41 | -------------------------------------------------------------------------------- /src/lib/server/tools/directlyAnswer.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigTool } from "$lib/types/Tool"; 2 | import { ObjectId } from "mongodb"; 3 | 4 | const directlyAnswer: ConfigTool = { 5 | _id: new ObjectId("00000000000000000000000D"), 6 | type: "config", 7 | description: 8 | "Answer the user's query directly. You must use this tool before you can answer the user's query.", 9 | color: "blue", 10 | icon: "chat", 11 | displayName: "Directly Answer", 12 | isOnByDefault: true, 13 | isLocked: true, 14 | isHidden: true, 15 | name: "directlyAnswer", 16 | endpoint: null, 17 | inputs: [], 18 | outputComponent: null, 19 | outputComponentIdx: null, 20 | showOutput: false, 21 | async *call() { 22 | return { 23 | outputs: [], 24 | display: false, 25 | }; 26 | }, 27 | }; 28 | 29 | export default directlyAnswer; 30 | -------------------------------------------------------------------------------- /src/lib/server/tools/outputs.ts: -------------------------------------------------------------------------------- 1 | import type { ToolIOType, ToolOutputComponents } from "$lib/types/Tool"; 2 | 3 | export const ToolOutputPaths: Record< 4 | ToolOutputComponents, 5 | { 6 | type: ToolIOType; 7 | path: string; 8 | } 9 | > = { 10 | textbox: { 11 | type: "str", 12 | path: "$", 13 | }, 14 | markdown: { 15 | type: "str", 16 | path: "$", 17 | }, 18 | number: { 19 | type: "float", 20 | path: "$", 21 | }, 22 | image: { 23 | type: "file", 24 | path: "$.url", 25 | }, 26 | gallery: { 27 | type: "file", 28 | path: "$[*].image.url", 29 | }, 30 | audio: { 31 | type: "file", 32 | path: "$.url", 33 | }, 34 | video: { 35 | type: "file", 36 | path: "$.video.url", 37 | }, 38 | file: { 39 | type: "file", 40 | path: "$.url", 41 | }, 42 | json: { 43 | type: "str", 44 | path: "$", 45 | }, 46 | }; 47 | 48 | export const isValidOutputComponent = ( 49 | outputComponent: string 50 | ): outputComponent is keyof typeof ToolOutputPaths => { 51 | return Object.keys(ToolOutputPaths).includes(outputComponent); 52 | }; 53 | -------------------------------------------------------------------------------- /src/lib/server/tools/web/url.ts: -------------------------------------------------------------------------------- 1 | import { stringifyMarkdownElementTree } from "$lib/server/websearch/markdown/utils/stringify"; 2 | import { scrapeUrl } from "$lib/server/websearch/scrape/scrape"; 3 | import type { ConfigTool } from "$lib/types/Tool"; 4 | import { ObjectId } from "mongodb"; 5 | 6 | const fetchUrl: ConfigTool = { 7 | _id: new ObjectId("00000000000000000000000B"), 8 | type: "config", 9 | description: "Fetch the contents of a URL", 10 | color: "blue", 11 | icon: "cloud", 12 | displayName: "Fetch URL", 13 | name: "fetchUrl", 14 | endpoint: null, 15 | inputs: [ 16 | { 17 | name: "url", 18 | type: "str", 19 | description: "The URL of the webpage to fetch", 20 | paramType: "required", 21 | }, 22 | ], 23 | outputComponent: null, 24 | outputComponentIdx: null, 25 | showOutput: false, 26 | async *call({ url }) { 27 | const blocks = String(url).split("\n"); 28 | const urlStr = blocks[blocks.length - 1]; 29 | 30 | const { title, markdownTree } = await scrapeUrl(urlStr, Infinity); 31 | 32 | return { 33 | outputs: [{ title, text: stringifyMarkdownElementTree(markdownTree) }], 34 | display: false, 35 | }; 36 | }, 37 | }; 38 | 39 | export default fetchUrl; 40 | -------------------------------------------------------------------------------- /src/lib/server/usageLimits.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { config } from "$lib/server/config"; 3 | import JSON5 from "json5"; 4 | 5 | // RATE_LIMIT is the legacy way to define messages per minute limit 6 | export const usageLimitsSchema = z 7 | .object({ 8 | conversations: z.coerce.number().optional(), // how many conversations 9 | messages: z.coerce.number().optional(), // how many messages in a conversation 10 | assistants: z.coerce.number().optional(), // how many assistants 11 | messageLength: z.coerce.number().optional(), // how long can a message be before we cut it off 12 | messagesPerMinute: z 13 | .preprocess((val) => { 14 | if (val === undefined) { 15 | return config.RATE_LIMIT; 16 | } 17 | return val; 18 | }, z.coerce.number().optional()) 19 | .optional(), // how many messages per minute 20 | tools: z.coerce.number().optional(), // how many tools 21 | }) 22 | .optional(); 23 | 24 | export const usageLimits = usageLimitsSchema.parse(JSON5.parse(config.USAGE_LIMITS)); 25 | -------------------------------------------------------------------------------- /src/lib/server/websearch/embed/combine.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingBackendModel } from "$lib/server/embeddingModels"; 2 | import { getSentenceSimilarity } from "$lib/server/sentenceSimilarity"; 3 | 4 | /** 5 | * Combines sentences together to reach the maximum character limit of the embedding model 6 | * Improves performance considerably when using CPU embedding 7 | */ 8 | export async function getCombinedSentenceSimilarity( 9 | embeddingModel: EmbeddingBackendModel, 10 | query: string, 11 | sentences: string[] 12 | ): ReturnType { 13 | const combinedSentences = sentences.reduce<{ text: string; indices: number[] }[]>( 14 | (acc, sentence, idx) => { 15 | const lastSentence = acc[acc.length - 1]; 16 | if (!lastSentence) return [{ text: sentence, indices: [idx] }]; 17 | if (lastSentence.text.length + sentence.length < embeddingModel.chunkCharLength) { 18 | lastSentence.text += ` ${sentence}`; 19 | lastSentence.indices.push(idx); 20 | return acc; 21 | } 22 | return [...acc, { text: sentence, indices: [idx] }]; 23 | }, 24 | [] 25 | ); 26 | 27 | const embeddings = await getSentenceSimilarity( 28 | embeddingModel, 29 | query, 30 | combinedSentences.map(({ text }) => text) 31 | ); 32 | 33 | return embeddings.flatMap((embedding, idx) => { 34 | const { indices } = combinedSentences[idx]; 35 | return indices.map((i) => ({ ...embedding, idx: i })); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/server/websearch/embed/tree.ts: -------------------------------------------------------------------------------- 1 | import type { MarkdownElement } from "../markdown/types"; 2 | 3 | export function flattenTree(elem: MarkdownElement): MarkdownElement[] { 4 | if ("children" in elem) return [elem, ...elem.children.flatMap(flattenTree)]; 5 | return [elem]; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/server/websearch/markdown/utils/nlp.ts: -------------------------------------------------------------------------------- 1 | /** Remove excess whitespace and newlines */ 2 | export const sanitizeString = (str: string) => 3 | str 4 | .split("\n") 5 | .map((s) => s.trim()) 6 | .filter(Boolean) 7 | .join("\n") 8 | .replaceAll(/ +/g, " "); 9 | 10 | /** Collapses a string into a single line */ 11 | export const collapseString = (str: string) => sanitizeString(str.replaceAll(/\n/g, " ")); 12 | -------------------------------------------------------------------------------- /src/lib/server/websearch/scrape/types.ts: -------------------------------------------------------------------------------- 1 | export interface SerializedHTMLElement { 2 | tagName: string; 3 | attributes: Record; 4 | content: (SerializedHTMLElement | string)[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/bing.ts: -------------------------------------------------------------------------------- 1 | import type { WebSearchSource } from "$lib/types/WebSearch"; 2 | import { config } from "$lib/server/config"; 3 | 4 | export default async function search(query: string): Promise { 5 | // const params = { 6 | // q: query, 7 | // // You can add other parameters if needed, like 'count', 'offset', etc. 8 | // }; 9 | 10 | const response = await fetch( 11 | "https://api.bing.microsoft.com/v7.0/search" + "?q=" + encodeURIComponent(query), 12 | { 13 | method: "GET", 14 | headers: { 15 | "Ocp-Apim-Subscription-Key": config.BING_SUBSCRIPTION_KEY, 16 | "Content-type": "application/json", 17 | }, 18 | } 19 | ); 20 | 21 | /* eslint-disable @typescript-eslint/no-explicit-any */ 22 | const data = (await response.json()) as Record; 23 | 24 | if (!response.ok) { 25 | throw new Error( 26 | data["message"] ?? `Bing API returned error code ${response.status} - ${response.statusText}` 27 | ); 28 | } 29 | 30 | // Adapt the data structure from the Bing response to match the WebSearchSource type 31 | const webPages = data["webPages"]?.["value"] ?? []; 32 | return webPages.map((page: any) => ({ 33 | title: page.name, 34 | link: page.url, 35 | text: page.snippet, 36 | displayLink: page.displayUrl, 37 | })); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/searchApi.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import type { WebSearchSource } from "$lib/types/WebSearch"; 3 | 4 | export default async function search(query: string): Promise { 5 | const response = await fetch( 6 | `https://www.searchapi.io/api/v1/search?engine=google&hl=en&gl=us&q=${query}`, 7 | { 8 | method: "GET", 9 | headers: { 10 | Authorization: `Bearer ${config.SEARCHAPI_KEY}`, 11 | "Content-type": "application/json", 12 | }, 13 | } 14 | ); 15 | 16 | /* eslint-disable @typescript-eslint/no-explicit-any */ 17 | const data = (await response.json()) as Record; 18 | 19 | if (!response.ok) { 20 | throw new Error( 21 | data["message"] ?? `SearchApi returned error code ${response.status} - ${response.statusText}` 22 | ); 23 | } 24 | 25 | return data["organic_results"] ?? []; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/searxng.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { logger } from "$lib/server/logger"; 3 | import type { WebSearchSource } from "$lib/types/WebSearch"; 4 | import { isURL } from "$lib/utils/isUrl"; 5 | 6 | export default async function searchSearxng(query: string): Promise { 7 | const abortController = new AbortController(); 8 | setTimeout(() => abortController.abort(), 10000); 9 | 10 | // Insert the query into the URL template 11 | let url = config.SEARXNG_QUERY_URL.replace("", query); 12 | 13 | // Check if "&format=json" already exists in the URL 14 | if (!url.includes("&format=json")) { 15 | url += "&format=json"; 16 | } 17 | 18 | // Call the URL to return JSON data 19 | const jsonResponse = await fetch(url, { 20 | signal: abortController.signal, 21 | }) 22 | .then((response) => response.json() as Promise<{ results: { url: string }[] }>) 23 | .catch((error) => { 24 | logger.error(error, "Failed to fetch or parse JSON"); 25 | throw new Error("Failed to fetch or parse JSON", { cause: error }); 26 | }); 27 | 28 | // Extract 'url' elements from the JSON response and trim to the top 5 URLs 29 | const urls = jsonResponse.results.slice(0, 5).map((item) => item.url); 30 | 31 | if (!urls.length) { 32 | throw new Error(`Response doesn't contain any "url" elements`); 33 | } 34 | 35 | // Map URLs to the correct object shape 36 | return urls.filter(isURL).map((link) => ({ link })); 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/serpApi.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { getJson, type GoogleParameters } from "serpapi"; 3 | import type { WebSearchSource } from "$lib/types/WebSearch"; 4 | import { isURL } from "$lib/utils/isUrl"; 5 | 6 | type SerpApiResponse = { 7 | organic_results: { 8 | link: string; 9 | }[]; 10 | }; 11 | 12 | export default async function searchWebSerpApi(query: string): Promise { 13 | const params = { 14 | q: query, 15 | hl: "en", 16 | gl: "us", 17 | google_domain: "google.com", 18 | api_key: config.SERPAPI_KEY, 19 | } satisfies GoogleParameters; 20 | 21 | // Show result as JSON 22 | const response = (await getJson("google", params)) as unknown as SerpApiResponse; 23 | 24 | return response.organic_results.filter(({ link }) => isURL(link)); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/serpStack.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { isURL } from "$lib/utils/isUrl"; 3 | import type { WebSearchSource } from "$lib/types/WebSearch"; 4 | 5 | type SerpStackResponse = { 6 | organic_results: { 7 | title: string; 8 | url: string; 9 | snippet?: string; 10 | }[]; 11 | error?: string; 12 | }; 13 | 14 | export default async function searchSerpStack(query: string): Promise { 15 | const response = await fetch( 16 | `http://api.serpstack.com/search?access_key=${config.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, 17 | { headers: { "Content-type": "application/json; charset=UTF-8" } } 18 | ); 19 | 20 | const data = (await response.json()) as SerpStackResponse; 21 | 22 | if (!response.ok) { 23 | throw new Error( 24 | data.error ?? `SerpStack API returned error code ${response.status} - ${response.statusText}` 25 | ); 26 | } 27 | 28 | return data.organic_results 29 | .filter(({ url }) => isURL(url)) 30 | .map(({ title, url, snippet }) => ({ 31 | title, 32 | link: url, 33 | text: snippet ?? "", 34 | })); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/serper.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import type { WebSearchSource } from "$lib/types/WebSearch"; 3 | 4 | export default async function search(query: string): Promise { 5 | const params = { 6 | q: query, 7 | hl: "en", 8 | gl: "us", 9 | }; 10 | 11 | const response = await fetch("https://google.serper.dev/search", { 12 | method: "POST", 13 | body: JSON.stringify(params), 14 | headers: { 15 | "x-api-key": config.SERPER_API_KEY, 16 | "Content-type": "application/json", 17 | }, 18 | }); 19 | 20 | /* eslint-disable @typescript-eslint/no-explicit-any */ 21 | const data = (await response.json()) as Record; 22 | 23 | if (!response.ok) { 24 | throw new Error( 25 | data["message"] ?? 26 | `Serper API returned error code ${response.status} - ${response.statusText}` 27 | ); 28 | } 29 | 30 | return data["organic"] ?? []; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/webLocal.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM, VirtualConsole } from "jsdom"; 2 | import { isURL } from "$lib/utils/isUrl"; 3 | import type { WebSearchSource } from "$lib/types/WebSearch"; 4 | 5 | export default async function searchWebLocal(query: string): Promise { 6 | const abortController = new AbortController(); 7 | setTimeout(() => abortController.abort(), 10000); 8 | 9 | const htmlString = await fetch( 10 | "https://www.google.com/search?hl=en&q=" + encodeURIComponent(query), 11 | { signal: abortController.signal } 12 | ) 13 | .then((response) => response.text()) 14 | .catch(); 15 | 16 | const virtualConsole = new VirtualConsole(); 17 | virtualConsole.on("error", () => {}); // No-op to skip console errors. 18 | const document = new JSDOM(htmlString ?? "", { virtualConsole }).window.document; 19 | 20 | // get all links 21 | const links = document.querySelectorAll("a"); 22 | if (!links.length) throw new Error(`webpage doesn't have any "a" element`); 23 | 24 | // take url that start wirth /url?q= 25 | // and do not contain google.com links 26 | // and strip them up to '&sa=' 27 | const linksHref = Array.from(links) 28 | .map((el) => el.href) 29 | .filter((link) => link.startsWith("/url?q=") && !link.includes("google.com/")) 30 | .map((link) => link.slice("/url?q=".length, link.indexOf("&sa="))) 31 | .filter(isURL); 32 | 33 | // remove duplicate links and map links to the correct object shape 34 | return [...new Set(linksHref)].map((link) => ({ link })); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/server/websearch/search/endpoints/youApi.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { isURL } from "$lib/utils/isUrl"; 3 | import type { WebSearchSource } from "$lib/types/WebSearch"; 4 | 5 | interface YouWebSearch { 6 | hits: YouSearchHit[]; 7 | latency: number; 8 | } 9 | 10 | interface YouSearchHit { 11 | url: string; 12 | title: string; 13 | description: string; 14 | snippets: string[]; 15 | } 16 | 17 | export default async function searchWebYouApi(query: string): Promise { 18 | const response = await fetch(`https://api.ydc-index.io/search?query=${query}`, { 19 | method: "GET", 20 | headers: { 21 | "X-API-Key": config.YDC_API_KEY, 22 | "Content-type": "application/json; charset=UTF-8", 23 | }, 24 | }); 25 | 26 | if (!response.ok) { 27 | throw new Error(`You.com API returned error code ${response.status} - ${response.statusText}`); 28 | } 29 | 30 | const data = (await response.json()) as YouWebSearch; 31 | const formattedResultsWithSnippets = data.hits 32 | .filter(({ url }) => isURL(url)) 33 | .map(({ title, url, snippets }) => ({ 34 | title, 35 | link: url, 36 | text: snippets?.join("\n") || "", 37 | })) 38 | .sort((a, b) => b.text.length - a.text.length); // desc order by text length 39 | 40 | return formattedResultsWithSnippets; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/server/websearch/update.ts: -------------------------------------------------------------------------------- 1 | import type { WebSearchSource } from "$lib/types/WebSearch"; 2 | import { 3 | MessageUpdateType, 4 | MessageWebSearchUpdateType, 5 | type MessageWebSearchErrorUpdate, 6 | type MessageWebSearchFinishedUpdate, 7 | type MessageWebSearchGeneralUpdate, 8 | type MessageWebSearchSourcesUpdate, 9 | } from "$lib/types/MessageUpdate"; 10 | 11 | export function makeGeneralUpdate( 12 | update: Pick 13 | ): MessageWebSearchGeneralUpdate { 14 | return { 15 | type: MessageUpdateType.WebSearch, 16 | subtype: MessageWebSearchUpdateType.Update, 17 | ...update, 18 | }; 19 | } 20 | 21 | export function makeErrorUpdate( 22 | update: Pick 23 | ): MessageWebSearchErrorUpdate { 24 | return { 25 | type: MessageUpdateType.WebSearch, 26 | subtype: MessageWebSearchUpdateType.Error, 27 | ...update, 28 | }; 29 | } 30 | 31 | export function makeSourcesUpdate(sources: WebSearchSource[]): MessageWebSearchSourcesUpdate { 32 | return { 33 | type: MessageUpdateType.WebSearch, 34 | subtype: MessageWebSearchUpdateType.Sources, 35 | message: "sources", 36 | sources: sources.map(({ link, title }) => ({ link, title })), 37 | }; 38 | } 39 | 40 | export function makeFinalAnswerUpdate(): MessageWebSearchFinishedUpdate { 41 | return { 42 | type: MessageUpdateType.WebSearch, 43 | subtype: MessageWebSearchUpdateType.Finished, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/shareConversation.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { ERROR_MESSAGES, error } from "$lib/stores/errors"; 3 | import { share } from "./utils/share"; 4 | import { page } from "$app/stores"; 5 | import { get } from "svelte/store"; 6 | import { getShareUrl } from "./utils/getShareUrl"; 7 | export async function shareConversation(id: string, title: string) { 8 | try { 9 | if (id.length === 7) { 10 | const url = get(page).url; 11 | await share(getShareUrl(url, id), title, true); 12 | } else { 13 | const res = await fetch(`${base}/conversation/${id}/share`, { 14 | method: "POST", 15 | headers: { 16 | "Content-Type": "application/json", 17 | }, 18 | }); 19 | 20 | if (!res.ok) { 21 | error.set("Error while sharing conversation, try again."); 22 | console.error("Error while sharing conversation: " + (await res.text())); 23 | return; 24 | } 25 | 26 | const { url } = await res.json(); 27 | await share(url, title, true); 28 | } 29 | } catch (err) { 30 | error.set(ERROR_MESSAGES.default); 31 | console.error(err); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/stores/errors.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const ERROR_MESSAGES = { 4 | default: "Oops, something went wrong.", 5 | authOnly: "You have to be logged in.", 6 | rateLimited: "You are sending too many messages. Try again later.", 7 | }; 8 | 9 | export const error = writable(undefined); 10 | -------------------------------------------------------------------------------- /src/lib/stores/isAborted.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const isAborted = writable(false); 4 | -------------------------------------------------------------------------------- /src/lib/stores/loginModal.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const loginModalOpen = writable(false); 4 | -------------------------------------------------------------------------------- /src/lib/stores/pendingMessage.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const pendingMessage = writable< 4 | | { 5 | content: string; 6 | files: File[]; 7 | } 8 | | undefined 9 | >(); 10 | -------------------------------------------------------------------------------- /src/lib/stores/titleUpdate.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export interface TitleUpdate { 4 | convId: string; 5 | title: string; 6 | } 7 | 8 | export default writable(null); 9 | -------------------------------------------------------------------------------- /src/lib/stores/webSearchParameters.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | export interface WebSearchParameters { 3 | useSearch: boolean; 4 | nItems: number; 5 | } 6 | export const webSearchParameters = writable({ 7 | useSearch: false, 8 | nItems: 5, 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/switchTheme.ts: -------------------------------------------------------------------------------- 1 | export function switchTheme() { 2 | const { classList } = document.querySelector("html") as HTMLElement; 3 | const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement; 4 | 5 | if (classList.contains("dark")) { 6 | classList.remove("dark"); 7 | metaTheme.setAttribute("content", "rgb(249, 250, 251)"); 8 | localStorage.theme = "light"; 9 | } else { 10 | classList.add("dark"); 11 | metaTheme.setAttribute("content", "rgb(26, 36, 50)"); 12 | localStorage.theme = "dark"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/types/AbortedGeneration.ts: -------------------------------------------------------------------------------- 1 | // Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 2 | 3 | import type { Conversation } from "./Conversation"; 4 | import type { Timestamps } from "./Timestamps"; 5 | 6 | export interface AbortedGeneration extends Timestamps { 7 | conversationId: Conversation["_id"]; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/types/Assistant.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | import type { User } from "./User"; 3 | import type { Timestamps } from "./Timestamps"; 4 | import type { ReviewStatus } from "./Review"; 5 | 6 | export interface Assistant extends Timestamps { 7 | _id: ObjectId; 8 | createdById: User["_id"] | string; // user id or session 9 | createdByName?: User["username"]; 10 | avatar?: string; 11 | name: string; 12 | description?: string; 13 | modelId: string; 14 | exampleInputs: string[]; 15 | preprompt: string; 16 | userCount?: number; 17 | review: ReviewStatus; 18 | rag?: { 19 | allowAllDomains: boolean; 20 | allowedDomains: string[]; 21 | allowedLinks: string[]; 22 | }; 23 | generateSettings?: { 24 | temperature?: number; 25 | top_p?: number; 26 | repetition_penalty?: number; 27 | top_k?: number; 28 | }; 29 | dynamicPrompt?: boolean; 30 | searchTokens: string[]; 31 | last24HoursCount: number; 32 | tools?: string[]; 33 | } 34 | 35 | // eslint-disable-next-line no-shadow 36 | export enum SortKey { 37 | POPULAR = "popular", 38 | TRENDING = "trending", 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/types/AssistantStats.ts: -------------------------------------------------------------------------------- 1 | import type { Timestamps } from "./Timestamps"; 2 | import type { Assistant } from "./Assistant"; 3 | 4 | export interface AssistantStats extends Timestamps { 5 | assistantId: Assistant["_id"]; 6 | date: { 7 | at: Date; 8 | span: "hour"; 9 | }; 10 | count: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/types/ConfigKey.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigKey { 2 | key: string; // unique 3 | value: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/types/ConvSidebar.ts: -------------------------------------------------------------------------------- 1 | export interface ConvSidebar { 2 | id: string; 3 | title: string; 4 | updatedAt: Date; 5 | model?: string; 6 | assistantId?: string; 7 | avatarUrl?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/types/Conversation.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | import type { Message } from "./Message"; 3 | import type { Timestamps } from "./Timestamps"; 4 | import type { User } from "./User"; 5 | import type { Assistant } from "./Assistant"; 6 | 7 | export interface Conversation extends Timestamps { 8 | _id: ObjectId; 9 | 10 | sessionId?: string; 11 | userId?: User["_id"]; 12 | 13 | model: string; 14 | embeddingModel: string; 15 | 16 | title: string; 17 | rootMessageId?: Message["id"]; 18 | messages: Message[]; 19 | 20 | meta?: { 21 | fromShareId?: string; 22 | }; 23 | 24 | preprompt?: string; 25 | assistantId?: Assistant["_id"]; 26 | 27 | userAgent?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/types/ConversationStats.ts: -------------------------------------------------------------------------------- 1 | import type { Timestamps } from "./Timestamps"; 2 | 3 | export interface ConversationStats extends Timestamps { 4 | date: { 5 | at: Date; 6 | span: "day" | "week" | "month"; 7 | field: "updatedAt" | "createdAt"; 8 | }; 9 | type: "conversation" | "message"; 10 | /** _id => number of conversations/messages in the month */ 11 | distinct: "sessionId" | "userId" | "userOrSessionId" | "_id"; 12 | count: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/types/Message.ts: -------------------------------------------------------------------------------- 1 | import type { MessageUpdate } from "./MessageUpdate"; 2 | import type { Timestamps } from "./Timestamps"; 3 | import type { WebSearch } from "./WebSearch"; 4 | import type { v4 } from "uuid"; 5 | 6 | export type Message = Partial & { 7 | from: "user" | "assistant" | "system"; 8 | id: ReturnType; 9 | content: string; 10 | updates?: MessageUpdate[]; 11 | webSearchId?: WebSearch["_id"]; // legacy version 12 | webSearch?: WebSearch; 13 | 14 | reasoning?: string; 15 | score?: -1 | 0 | 1; 16 | /** 17 | * Either contains the base64 encoded image data 18 | * or the hash of the file stored on the server 19 | **/ 20 | files?: MessageFile[]; 21 | interrupted?: boolean; 22 | 23 | // needed for conversation trees 24 | ancestors?: Message["id"][]; 25 | 26 | // goes one level deep 27 | children?: Message["id"][]; 28 | }; 29 | 30 | export type MessageFile = { 31 | type: "hash" | "base64"; 32 | name: string; 33 | value: string; 34 | mime: string; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/types/MessageEvent.ts: -------------------------------------------------------------------------------- 1 | import type { Session } from "./Session"; 2 | import type { Timestamps } from "./Timestamps"; 3 | import type { User } from "./User"; 4 | 5 | export interface MessageEvent extends Pick { 6 | userId: User["_id"] | Session["sessionId"]; 7 | ip?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/types/MigrationResult.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | 3 | export interface MigrationResult { 4 | _id: ObjectId; 5 | name: string; 6 | status: "success" | "failure" | "ongoing"; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/types/Model.ts: -------------------------------------------------------------------------------- 1 | import type { BackendModel } from "$lib/server/models"; 2 | 3 | export type Model = Pick< 4 | BackendModel, 5 | | "id" 6 | | "name" 7 | | "displayName" 8 | | "websiteUrl" 9 | | "datasetName" 10 | | "promptExamples" 11 | | "parameters" 12 | | "description" 13 | | "logoUrl" 14 | | "modelUrl" 15 | | "tokenizer" 16 | | "datasetUrl" 17 | | "preprompt" 18 | | "multimodal" 19 | | "multimodalAcceptedMimetypes" 20 | | "unlisted" 21 | | "tools" 22 | | "hasInferenceAPI" 23 | >; 24 | -------------------------------------------------------------------------------- /src/lib/types/Report.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | import type { User } from "./User"; 3 | import type { Assistant } from "./Assistant"; 4 | import type { Timestamps } from "./Timestamps"; 5 | 6 | export interface Report extends Timestamps { 7 | _id: ObjectId; 8 | createdBy: User["_id"] | string; 9 | object: "assistant" | "tool"; 10 | contentId: Assistant["_id"]; 11 | reason?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/types/Review.ts: -------------------------------------------------------------------------------- 1 | export enum ReviewStatus { 2 | PRIVATE = "PRIVATE", 3 | PENDING = "PENDING", 4 | APPROVED = "APPROVED", 5 | DENIED = "DENIED", 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/types/Semaphore.ts: -------------------------------------------------------------------------------- 1 | import type { Timestamps } from "./Timestamps"; 2 | 3 | export interface Semaphore extends Timestamps { 4 | key: string; 5 | deleteAt: Date; 6 | } 7 | 8 | export enum Semaphores { 9 | ASSISTANTS_COUNT = "assistants.count", 10 | CONVERSATION_STATS = "conversation.stats", 11 | CONFIG_UPDATE = "config.update", 12 | MIGRATION = "migration", 13 | TEST_MIGRATION = "test.migration", 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/types/Session.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "bson"; 2 | import type { Timestamps } from "./Timestamps"; 3 | import type { User } from "./User"; 4 | 5 | export interface Session extends Timestamps { 6 | _id: ObjectId; 7 | sessionId: string; 8 | userId: User["_id"]; 9 | userAgent?: string; 10 | ip?: string; 11 | expiresAt: Date; 12 | admin?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/types/Settings.ts: -------------------------------------------------------------------------------- 1 | import { defaultModel } from "$lib/server/models"; 2 | import type { Assistant } from "./Assistant"; 3 | import type { Timestamps } from "./Timestamps"; 4 | import type { User } from "./User"; 5 | 6 | export interface Settings extends Timestamps { 7 | userId?: User["_id"]; 8 | sessionId?: string; 9 | 10 | /** 11 | * Note: Only conversations with this settings explicitly set to true should be shared. 12 | * 13 | * This setting is explicitly set to true when users accept the ethics modal. 14 | * */ 15 | shareConversationsWithModelAuthors: boolean; 16 | ethicsModalAcceptedAt: Date | null; 17 | activeModel: string; 18 | hideEmojiOnSidebar?: boolean; 19 | 20 | // model name and system prompts 21 | customPrompts?: Record; 22 | 23 | assistants?: Assistant["_id"][]; 24 | tools?: string[]; 25 | disableStream: boolean; 26 | directPaste: boolean; 27 | } 28 | 29 | export type SettingsEditable = Omit; 30 | // TODO: move this to a constant file along with other constants 31 | export const DEFAULT_SETTINGS = { 32 | shareConversationsWithModelAuthors: true, 33 | activeModel: defaultModel.id, 34 | hideEmojiOnSidebar: false, 35 | customPrompts: {}, 36 | assistants: [], 37 | tools: [], 38 | disableStream: false, 39 | directPaste: false, 40 | } satisfies SettingsEditable; 41 | -------------------------------------------------------------------------------- /src/lib/types/SharedConversation.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "./Conversation"; 2 | 3 | export type SharedConversation = Pick< 4 | Conversation, 5 | | "model" 6 | | "embeddingModel" 7 | | "title" 8 | | "rootMessageId" 9 | | "messages" 10 | | "preprompt" 11 | | "assistantId" 12 | | "createdAt" 13 | | "updatedAt" 14 | > & { 15 | _id: string; 16 | hash: string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/types/Template.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "./Message"; 2 | import type { Tool, ToolResult } from "./Tool"; 3 | 4 | export type ChatTemplateInput = { 5 | messages: Pick[]; 6 | preprompt?: string; 7 | tools?: Tool[]; 8 | toolResults?: ToolResult[]; 9 | continueMessage?: boolean; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/types/Timestamps.ts: -------------------------------------------------------------------------------- 1 | export interface Timestamps { 2 | createdAt: Date; 3 | updatedAt: Date; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/types/TokenCache.ts: -------------------------------------------------------------------------------- 1 | import type { Timestamps } from "./Timestamps"; 2 | 3 | export interface TokenCache extends Timestamps { 4 | tokenHash: string; // sha256 of the bearer token 5 | userId: string; // the matching hf user id 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/types/UrlDependency.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | export enum UrlDependency { 3 | ConversationList = "conversation:list", 4 | Conversation = "conversation", 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/types/User.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | import type { Timestamps } from "./Timestamps"; 3 | 4 | export interface User extends Timestamps { 5 | _id: ObjectId; 6 | 7 | username?: string; 8 | name: string; 9 | email?: string; 10 | avatarUrl: string | undefined; 11 | hfUserId: string; 12 | isAdmin?: boolean; 13 | isEarlyAccess?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/types/WebSearch.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from "mongodb"; 2 | import type { Conversation } from "./Conversation"; 3 | import type { Timestamps } from "./Timestamps"; 4 | import type { HeaderElement } from "$lib/server/websearch/markdown/types"; 5 | 6 | export interface WebSearch extends Timestamps { 7 | _id?: ObjectId; 8 | convId?: Conversation["_id"]; 9 | 10 | prompt: string; 11 | 12 | searchQuery: string; 13 | results: WebSearchSource[]; 14 | contextSources: WebSearchUsedSource[]; 15 | } 16 | 17 | export interface WebSearchSource { 18 | title?: string; 19 | link: string; 20 | } 21 | export interface WebSearchScrapedSource extends WebSearchSource { 22 | page: WebSearchPage; 23 | } 24 | export interface WebSearchPage { 25 | title: string; 26 | siteName?: string; 27 | author?: string; 28 | description?: string; 29 | createdAt?: string; 30 | modifiedAt?: string; 31 | markdownTree: HeaderElement; 32 | } 33 | 34 | export interface WebSearchUsedSource extends WebSearchScrapedSource { 35 | context: string; 36 | } 37 | 38 | export type WebSearchMessageSources = { 39 | type: "sources"; 40 | sources: WebSearchSource[]; 41 | }; 42 | 43 | // eslint-disable-next-line no-shadow 44 | export enum WebSearchProvider { 45 | GOOGLE = "Google", 46 | YOU = "You.com", 47 | SEARXNG = "SearXNG", 48 | BING = "Bing", 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/utils/PublicConfig.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { env as publicEnv } from "$env/dynamic/public"; 2 | 3 | type PublicConfigKey = keyof typeof publicEnv; 4 | 5 | class PublicConfigManager { 6 | #configStore = $state>({}); 7 | 8 | constructor() { 9 | this.init = this.init.bind(this); 10 | } 11 | 12 | init(publicConfig: Record) { 13 | this.#configStore = publicConfig; 14 | } 15 | 16 | get(key: PublicConfigKey) { 17 | return this.#configStore[key]; 18 | } 19 | 20 | get isHuggingChat() { 21 | return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat"; 22 | } 23 | } 24 | 25 | const publicConfigManager = new PublicConfigManager(); 26 | 27 | type ConfigProxy = PublicConfigManager & { [K in PublicConfigKey]: string }; 28 | 29 | export const publicConfig: ConfigProxy = new Proxy(publicConfigManager, { 30 | get(target, prop) { 31 | if (prop in target) { 32 | return Reflect.get(target, prop); 33 | } 34 | if (typeof prop === "string") { 35 | return target.get(prop as PublicConfigKey); 36 | } 37 | return undefined; 38 | }, 39 | set(target, prop, value, receiver) { 40 | if (prop in target) { 41 | return Reflect.set(target, prop, value, receiver); 42 | } 43 | return false; 44 | }, 45 | }) as ConfigProxy; 46 | -------------------------------------------------------------------------------- /src/lib/utils/chunk.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Chunk array into arrays of length at most `chunkSize` 3 | * 4 | * @param chunkSize must be greater than or equal to 1 5 | */ 6 | export function chunk(arr: T, chunkSize: number): T[] { 7 | if (isNaN(chunkSize) || chunkSize < 1) { 8 | throw new RangeError("Invalid chunk size: " + chunkSize); 9 | } 10 | 11 | if (!arr.length) { 12 | return []; 13 | } 14 | 15 | /// Small optimization to not chunk buffers unless needed 16 | if (arr.length <= chunkSize) { 17 | return [arr]; 18 | } 19 | 20 | return range(Math.ceil(arr.length / chunkSize)).map((i) => { 21 | return arr.slice(i * chunkSize, (i + 1) * chunkSize); 22 | }) as T[]; 23 | } 24 | 25 | function range(n: number, b?: number): number[] { 26 | return b 27 | ? Array(b - n) 28 | .fill(0) 29 | .map((_, i) => n + i) 30 | : Array(n) 31 | .fill(0) 32 | .map((_, i) => i); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/utils/cookiesAreEnabled.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | 3 | export function cookiesAreEnabled(): boolean { 4 | if (!browser) return false; 5 | if (navigator.cookieEnabled) return navigator.cookieEnabled; 6 | 7 | // Create cookie 8 | document.cookie = "cookietest=1"; 9 | const ret = document.cookie.indexOf("cookietest=") != -1; 10 | // Delete cookie 11 | document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT"; 12 | return ret; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A debounce function that works in both browser and Nodejs. 3 | * For pure Nodejs work, prefer the `Debouncer` class. 4 | */ 5 | export function debounce( 6 | callback: (...rest: T) => unknown, 7 | limit: number 8 | ): (...rest: T) => void { 9 | let timer: ReturnType; 10 | 11 | return function (...rest) { 12 | clearTimeout(timer); 13 | timer = setTimeout(() => { 14 | callback(...rest); 15 | }, limit); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/utils/deepestChild.ts: -------------------------------------------------------------------------------- 1 | export function deepestChild(el: HTMLElement): HTMLElement { 2 | if (el.lastElementChild && el.lastElementChild.nodeType !== Node.TEXT_NODE) { 3 | return deepestChild(el.lastElementChild as HTMLElement); 4 | } 5 | return el; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils/file2base64.ts: -------------------------------------------------------------------------------- 1 | const file2base64 = (file: File): Promise => { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | reader.readAsDataURL(file); 5 | reader.onload = () => { 6 | const dataUrl = reader.result as string; 7 | const base64 = dataUrl.split(",")[1]; 8 | resolve(base64); 9 | }; 10 | reader.onerror = (error) => reject(error); 11 | }); 12 | }; 13 | 14 | export default file2base64; 15 | -------------------------------------------------------------------------------- /src/lib/utils/getGradioApi.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import type { Client } from "@gradio/client"; 3 | 4 | export type ApiReturnType = Awaited>; 5 | 6 | export async function getGradioApi(space: string) { 7 | const api: ApiReturnType = await fetch(`${base}/api/spaces-config?space=${space}`).then( 8 | async (res) => { 9 | if (!res.ok) { 10 | throw new Error(await res.text()); 11 | } 12 | return res.json(); 13 | } 14 | ); 15 | return api; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/getHref.ts: -------------------------------------------------------------------------------- 1 | export function getHref( 2 | url: URL | string, 3 | modifications: { 4 | newKeys?: Record; 5 | existingKeys?: { behaviour: "delete_except" | "delete"; keys: string[] }; 6 | } 7 | ) { 8 | const newUrl = new URL(url); 9 | const { newKeys, existingKeys } = modifications; 10 | 11 | // exsiting keys logic 12 | if (existingKeys) { 13 | const { behaviour, keys } = existingKeys; 14 | if (behaviour === "delete") { 15 | for (const key of keys) { 16 | newUrl.searchParams.delete(key); 17 | } 18 | } else { 19 | // delete_except 20 | const keysToPreserve = keys; 21 | for (const key of [...newUrl.searchParams.keys()]) { 22 | if (!keysToPreserve.includes(key)) { 23 | newUrl.searchParams.delete(key); 24 | } 25 | } 26 | } 27 | } 28 | 29 | // new keys logic 30 | if (newKeys) { 31 | for (const [key, val] of Object.entries(newKeys)) { 32 | if (val) { 33 | newUrl.searchParams.set(key, val); 34 | } else { 35 | newUrl.searchParams.delete(key); 36 | } 37 | } 38 | } 39 | 40 | return newUrl.toString(); 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/utils/getReturnFromGenerator.ts: -------------------------------------------------------------------------------- 1 | export async function getReturnFromGenerator(generator: AsyncGenerator): Promise { 2 | let result: IteratorResult; 3 | do { 4 | result = await generator.next(); 5 | } while (!result.done); // Keep calling `next()` until `done` is true 6 | return result.value; // Return the final value 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils/getShareUrl.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { publicConfig } from "$lib/utils/PublicConfig.svelte"; 3 | 4 | export function getShareUrl(url: URL, shareId: string): string { 5 | return `${ 6 | publicConfig.PUBLIC_SHARE_PREFIX || `${publicConfig.PUBLIC_ORIGIN || url.origin}${base}` 7 | }/r/${shareId}`; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils/getTokenizer.ts: -------------------------------------------------------------------------------- 1 | import type { Model } from "$lib/types/Model"; 2 | import { AutoTokenizer, PreTrainedTokenizer } from "@huggingface/transformers"; 3 | 4 | export async function getTokenizer(_modelTokenizer: Exclude) { 5 | if (typeof _modelTokenizer === "string") { 6 | // return auto tokenizer 7 | return await AutoTokenizer.from_pretrained(_modelTokenizer); 8 | } else { 9 | // construct & return pretrained tokenizer 10 | const { tokenizerUrl, tokenizerConfigUrl } = _modelTokenizer satisfies { 11 | tokenizerUrl: string; 12 | tokenizerConfigUrl: string; 13 | }; 14 | const tokenizerJSON = await (await fetch(tokenizerUrl)).json(); 15 | const tokenizerConfig = await (await fetch(tokenizerConfigUrl)).json(); 16 | return new PreTrainedTokenizer(tokenizerJSON, tokenizerConfig); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/utils/hashConv.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import { sha256 } from "./sha256"; 3 | 4 | export async function hashConv(conv: Conversation) { 5 | // messages contains the conversation message but only the immutable part 6 | const messages = conv.messages.map((message) => { 7 | return (({ from, id, content, webSearchId }) => ({ from, id, content, webSearchId }))(message); 8 | }); 9 | 10 | const hash = await sha256(JSON.stringify(messages)); 11 | return hash; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/utils/isDesktop.ts: -------------------------------------------------------------------------------- 1 | // Approximate width from which we disable autofocus 2 | const TABLET_VIEWPORT_WIDTH = 768; 3 | 4 | export function isDesktop(window: Window) { 5 | const { innerWidth } = window; 6 | return innerWidth > TABLET_VIEWPORT_WIDTH; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils/isUrl.ts: -------------------------------------------------------------------------------- 1 | export function isURL(url: string) { 2 | try { 3 | new URL(url); 4 | return true; 5 | } catch (e) { 6 | return false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils/isVirtualKeyboard.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | 3 | export function isVirtualKeyboard(): boolean { 4 | if (!browser) return false; 5 | 6 | // Check for touch capability 7 | if (navigator.maxTouchPoints > 0 && screen.width <= 768) return true; 8 | 9 | // Check for touch events 10 | if ("ontouchstart" in window) return true; 11 | 12 | // Fallback to user agent string check 13 | const userAgent = navigator.userAgent.toLowerCase(); 14 | 15 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/mergeAsyncGenerators.ts: -------------------------------------------------------------------------------- 1 | type Gen = AsyncGenerator; 2 | 3 | type GenPromiseMap = Map< 4 | Gen, 5 | Promise<{ gen: Gen } & IteratorResult> 6 | >; 7 | 8 | /** Merges multiple async generators into a single async generator that yields values from all of them in parallel. */ 9 | export async function* mergeAsyncGenerators( 10 | generators: Gen[] 11 | ): Gen { 12 | const promises: GenPromiseMap = new Map(); 13 | const results: Map, TReturn> = new Map(); 14 | 15 | for (const gen of generators) { 16 | promises.set( 17 | gen, 18 | gen.next().then((result) => ({ gen, ...result })) 19 | ); 20 | } 21 | 22 | while (promises.size) { 23 | const { gen, value, done } = await Promise.race(promises.values()); 24 | if (done) { 25 | results.set(gen, value as TReturn); 26 | promises.delete(gen); 27 | } else { 28 | promises.set( 29 | gen, 30 | gen.next().then((result) => ({ gen, ...result })) 31 | ); 32 | yield value as T; 33 | } 34 | } 35 | 36 | const orderedResults = generators.map((gen) => results.get(gen) as TReturn); 37 | return orderedResults; 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/utils/models.ts: -------------------------------------------------------------------------------- 1 | import type { Model } from "$lib/types/Model"; 2 | 3 | export const findCurrentModel = (models: Model[], id?: string): Model => 4 | models.find((m) => m.id === id) ?? models[0]; 5 | -------------------------------------------------------------------------------- /src/lib/utils/parseStringToList.ts: -------------------------------------------------------------------------------- 1 | export function parseStringToList(links: unknown): string[] { 2 | if (typeof links !== "string") { 3 | throw new Error("Expected a string"); 4 | } 5 | 6 | return links 7 | .split(",") 8 | .map((link) => link.trim()) 9 | .filter((link) => link.length > 0); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/utils/randomUuid.ts: -------------------------------------------------------------------------------- 1 | type UUID = ReturnType; 2 | 3 | export function randomUUID(): UUID { 4 | // Only on old safari / ios 5 | if (!("randomUUID" in crypto)) { 6 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => 7 | ( 8 | Number(c) ^ 9 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4))) 10 | ).toString(16) 11 | ) as UUID; 12 | } 13 | return crypto.randomUUID(); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/utils/screenshot.ts: -------------------------------------------------------------------------------- 1 | export async function captureScreen(): Promise { 2 | let stream: MediaStream | undefined; 3 | try { 4 | // This will show the native browser dialog for screen capture 5 | stream = await navigator.mediaDevices.getDisplayMedia({ 6 | video: true, 7 | audio: false, 8 | }); 9 | 10 | // Create a canvas element to capture the screenshot 11 | const canvas = document.createElement("canvas"); 12 | const video = document.createElement("video"); 13 | 14 | // Wait for the video to load metadata 15 | await new Promise((resolve) => { 16 | video.onloadedmetadata = () => { 17 | canvas.width = video.videoWidth; 18 | canvas.height = video.videoHeight; 19 | video.play(); 20 | resolve(null); 21 | }; 22 | if (stream) { 23 | video.srcObject = stream; 24 | } else { 25 | throw Error("No stream available"); 26 | } 27 | }); 28 | 29 | // Draw the video frame to canvas 30 | const context = canvas.getContext("2d"); 31 | context?.drawImage(video, 0, 0, canvas.width, canvas.height); 32 | // Convert to base64 33 | return canvas.toDataURL("image/png"); 34 | } catch (error) { 35 | console.error("Error capturing screenshot:", error); 36 | throw error; 37 | } finally { 38 | // Stop all tracks 39 | if (stream) { 40 | stream.getTracks().forEach((track) => track.stop()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/utils/searchTokens.ts: -------------------------------------------------------------------------------- 1 | const PUNCTUATION_REGEX = /\p{P}/gu; 2 | 3 | function removeDiacritics(s: string, form: "NFD" | "NFKD" = "NFD"): string { 4 | return s.normalize(form).replace(/[\u0300-\u036f]/g, ""); 5 | } 6 | 7 | export function generateSearchTokens(value: string): string[] { 8 | const fullTitleToken = removeDiacritics(value) 9 | .replace(PUNCTUATION_REGEX, "") 10 | .replaceAll(/\s+/g, "") 11 | .toLowerCase(); 12 | return [ 13 | ...new Set([ 14 | ...removeDiacritics(value) 15 | .split(/\s+/) 16 | .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) 17 | .filter((word) => word.length), 18 | ...(fullTitleToken.length ? [fullTitleToken] : []), 19 | ]), 20 | ]; 21 | } 22 | 23 | function escapeForRegExp(s: string): string { 24 | return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 25 | } 26 | 27 | export function generateQueryTokens(query: string): RegExp[] { 28 | return removeDiacritics(query) 29 | .split(/\s+/) 30 | .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) 31 | .filter((word) => word.length) 32 | .map((token) => new RegExp(`^${escapeForRegExp(token)}`)); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/utils/sha256.ts: -------------------------------------------------------------------------------- 1 | export async function sha256(input: string): Promise { 2 | const utf8 = new TextEncoder().encode(input); 3 | const hashBuffer = await crypto.subtle.digest("SHA-256", utf8); 4 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 5 | const hashHex = hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join(""); 6 | return hashHex; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils/share.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "$app/environment"; 2 | import { isDesktop } from "./isDesktop"; 3 | 4 | export async function share(url: string, title: string, appendLeafId: boolean = false) { 5 | if (!browser) return; 6 | 7 | // Retrieve the leafId from localStorage 8 | const leafId = localStorage.getItem("leafId"); 9 | 10 | if (appendLeafId && leafId) { 11 | // Use URL and URLSearchParams to add the leafId parameter 12 | const shareUrl = new URL(url); 13 | shareUrl.searchParams.append("leafId", leafId); 14 | url = shareUrl.toString(); 15 | } 16 | 17 | if (navigator.share && !isDesktop(window)) { 18 | navigator.share({ url, title }); 19 | } else { 20 | // this is really ugly 21 | // but on chrome the clipboard write doesn't work if the window isn't focused 22 | // and after we use confirm() to ask the user if they want to share, the window is no longer focused 23 | // for a few ms until the confirm dialog closes. tried await tick(), tried window.focus(), didnt work 24 | // bug doesnt occur in firefox, if you can find a better fix for it please do 25 | await new Promise((resolve) => setTimeout(resolve, 250)); 26 | await navigator.clipboard.writeText(url); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/utils/stringifyError.ts: -------------------------------------------------------------------------------- 1 | /** Takes an unknown error and attempts to convert it to a string */ 2 | export function stringifyError(error: unknown): string { 3 | if (error instanceof Error) return error.message; 4 | if (typeof error === "string") return error; 5 | if (typeof error === "object" && error !== null) { 6 | // try a few common properties 7 | if ("message" in error && typeof error.message === "string") return error.message; 8 | if ("body" in error && typeof error.body === "string") return error.body; 9 | if ("name" in error && typeof error.name === "string") return error.name; 10 | } 11 | return "Unknown error"; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/utils/sum.ts: -------------------------------------------------------------------------------- 1 | export function sum(nums: number[]): number { 2 | return nums.reduce((a, b) => a + b, 0); 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | export const timeout = (prom: Promise, time: number): Promise => { 2 | let timer: NodeJS.Timeout; 3 | return Promise.race([ 4 | prom, 5 | new Promise((_, reject) => { 6 | timer = setTimeout(() => reject(new Error(`Timeout after ${time / 1000} seconds`)), time); 7 | }), 8 | ]).finally(() => clearTimeout(timer)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/utils/toolIds.ts: -------------------------------------------------------------------------------- 1 | export const webSearchToolId = "00000000000000000000000a"; 2 | export const fetchUrlToolId = "00000000000000000000000b"; 3 | export const imageGenToolId = "000000000000000000000001"; 4 | export const documentParserToolId = "000000000000000000000002"; 5 | -------------------------------------------------------------------------------- /src/lib/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import type { Tool } from "$lib/types/Tool"; 2 | 3 | /** 4 | * Checks if a tool's name equals a value. Replaces all hyphens with underscores before comparison 5 | * since some models return underscores even when hyphens are used in the request. 6 | **/ 7 | export function toolHasName(name: string, tool: Pick): boolean { 8 | return tool.name.replaceAll("-", "_") === name.replaceAll("-", "_"); 9 | } 10 | 11 | export const colors = ["purple", "blue", "green", "yellow", "red"] as const; 12 | 13 | export const icons = [ 14 | "wikis", 15 | "tools", 16 | "camera", 17 | "code", 18 | "email", 19 | "cloud", 20 | "terminal", 21 | "game", 22 | "chat", 23 | "speaker", 24 | "video", 25 | ] as const; 26 | -------------------------------------------------------------------------------- /src/lib/utils/tree/addChildren.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import type { Message } from "$lib/types/Message"; 3 | import { v4 } from "uuid"; 4 | 5 | export function addChildren( 6 | conv: Pick, 7 | message: Omit, 8 | parentId?: Message["id"] 9 | ): Message["id"] { 10 | // if this is the first message we just push it 11 | if (conv.messages.length === 0) { 12 | const messageId = v4(); 13 | conv.rootMessageId = messageId; 14 | conv.messages.push({ 15 | ...message, 16 | ancestors: [], 17 | id: messageId, 18 | }); 19 | return messageId; 20 | } 21 | 22 | if (!parentId) { 23 | throw new Error("You need to specify a parentId if this is not the first message"); 24 | } 25 | 26 | const messageId = v4(); 27 | if (!conv.rootMessageId) { 28 | // if there is no parentId we just push the message 29 | if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) { 30 | throw new Error("This is a legacy conversation, you can only append to the last message"); 31 | } 32 | conv.messages.push({ ...message, id: messageId }); 33 | return messageId; 34 | } 35 | 36 | const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId]; 37 | conv.messages.push({ 38 | ...message, 39 | ancestors, 40 | id: messageId, 41 | children: [], 42 | }); 43 | 44 | const parent = conv.messages.find((m) => m.id === parentId); 45 | 46 | if (parent) { 47 | if (parent.children) { 48 | parent.children.push(messageId); 49 | } else parent.children = [messageId]; 50 | } 51 | 52 | return messageId; 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/utils/tree/addSibling.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import type { Message } from "$lib/types/Message"; 3 | import { v4 } from "uuid"; 4 | 5 | export function addSibling( 6 | conv: Pick, 7 | message: Omit, 8 | siblingId: Message["id"] 9 | ): Message["id"] { 10 | if (conv.messages.length === 0) { 11 | throw new Error("Cannot add a sibling to an empty conversation"); 12 | } 13 | if (!conv.rootMessageId) { 14 | throw new Error("Cannot add a sibling to a legacy conversation"); 15 | } 16 | 17 | const sibling = conv.messages.find((m) => m.id === siblingId); 18 | 19 | if (!sibling) { 20 | throw new Error("The sibling message doesn't exist"); 21 | } 22 | 23 | if (!sibling.ancestors || sibling.ancestors?.length === 0) { 24 | throw new Error("The sibling message is the root message, therefore we can't add a sibling"); 25 | } 26 | 27 | const messageId = v4(); 28 | 29 | conv.messages.push({ 30 | ...message, 31 | id: messageId, 32 | ancestors: sibling.ancestors, 33 | children: [], 34 | }); 35 | 36 | const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1]; 37 | const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId); 38 | 39 | if (nearestAncestor) { 40 | if (nearestAncestor.children) { 41 | nearestAncestor.children.push(messageId); 42 | } else nearestAncestor.children = [messageId]; 43 | } 44 | 45 | return messageId; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/utils/tree/buildSubtree.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import type { Message } from "$lib/types/Message"; 3 | 4 | export function buildSubtree( 5 | conv: Pick, 6 | id: Message["id"] 7 | ): Message[] { 8 | if (!conv.rootMessageId) { 9 | if (conv.messages.length === 0) return []; 10 | // legacy conversation slice up to id 11 | const index = conv.messages.findIndex((m) => m.id === id); 12 | if (index === -1) throw new Error("Message not found"); 13 | return conv.messages.slice(0, index + 1); 14 | } else { 15 | // find the message with the right id then create the ancestor tree 16 | const message = conv.messages.find((m) => m.id === id); 17 | if (!message) throw new Error("Message not found"); 18 | 19 | return [ 20 | ...(message.ancestors?.map((ancestorId) => { 21 | const ancestor = conv.messages.find((m) => m.id === ancestorId); 22 | if (!ancestor) throw new Error("Ancestor not found"); 23 | return ancestor; 24 | }) ?? []), 25 | message, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/utils/tree/convertLegacyConversation.spec.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "$lib/server/database"; 2 | import { ObjectId } from "mongodb"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { convertLegacyConversation } from "./convertLegacyConversation"; 6 | import { insertLegacyConversation } from "./treeHelpers.spec"; 7 | 8 | describe("convertLegacyConversation", () => { 9 | it("should convert a legacy conversation", async () => { 10 | const convId = await insertLegacyConversation(); 11 | const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); 12 | if (!conv) throw new Error("Conversation not found"); 13 | 14 | const newConv = convertLegacyConversation(conv); 15 | 16 | expect(newConv.rootMessageId).toBe(newConv.messages[0].id); 17 | expect(newConv.messages[0].ancestors).toEqual([]); 18 | expect(newConv.messages[1].ancestors).toEqual([newConv.messages[0].id]); 19 | expect(newConv.messages[0].children).toEqual([newConv.messages[1].id]); 20 | }); 21 | it("should work on empty conversations", async () => { 22 | const conv = { 23 | _id: new ObjectId(), 24 | rootMessageId: undefined, 25 | messages: [], 26 | }; 27 | const newConv = convertLegacyConversation(conv); 28 | expect(newConv.rootMessageId).toBe(undefined); 29 | expect(newConv.messages).toEqual([]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/lib/utils/tree/convertLegacyConversation.ts: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "$lib/types/Conversation"; 2 | import type { Message } from "$lib/types/Message"; 3 | import { v4 } from "uuid"; 4 | 5 | export function convertLegacyConversation( 6 | conv: Pick 7 | ): Pick { 8 | if (conv.rootMessageId) return conv; // not a legacy conversation 9 | if (conv.messages.length === 0) return conv; // empty conversation 10 | const messages = [ 11 | { 12 | from: "system", 13 | content: conv.preprompt ?? "", 14 | createdAt: new Date(), 15 | updatedAt: new Date(), 16 | id: v4(), 17 | } satisfies Message, 18 | ...conv.messages, 19 | ]; 20 | 21 | const rootMessageId = messages[0].id; 22 | 23 | const newMessages = messages.map((message, index) => { 24 | return { 25 | ...message, 26 | ancestors: messages.slice(0, index).map((m) => m.id), 27 | children: index < messages.length - 1 ? [messages[index + 1].id] : [], 28 | }; 29 | }); 30 | 31 | return { 32 | ...conv, 33 | rootMessageId, 34 | messages: newMessages, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/utils/tree/isMessageId.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { isMessageId } from "./isMessageId"; 3 | import { v4 } from "uuid"; 4 | 5 | describe("isMessageId", () => { 6 | it("should return true for a valid message id", () => { 7 | expect(isMessageId(v4())).toBe(true); 8 | }); 9 | it("should return false for an invalid message id", () => { 10 | expect(isMessageId("1-2-3-4")).toBe(false); 11 | }); 12 | it("should return false for an empty string", () => { 13 | expect(isMessageId("")).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/utils/tree/isMessageId.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "$lib/types/Message"; 2 | 3 | export function isMessageId(id: string): id is Message["id"] { 4 | return id.split("-").length === 5; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/utils/updates.ts: -------------------------------------------------------------------------------- 1 | // This is a debouncer for the updates from the server to the client 2 | // It is used to prevent the client from being overloaded with too many updates 3 | // It works by keeping track of the time it takes to render the updates 4 | // and adding a safety margin to it, to find the debounce time. 5 | 6 | class UpdateDebouncer { 7 | private renderStartedAt: Date | null = null; 8 | private lastRenderTimes: number[] = []; 9 | 10 | get maxUpdateTime() { 11 | if (this.lastRenderTimes.length === 0) { 12 | return 50; 13 | } 14 | 15 | const averageTime = 16 | this.lastRenderTimes.reduce((acc, time) => acc + time, 0) / this.lastRenderTimes.length; 17 | 18 | return Math.min(averageTime * 3, 500); 19 | } 20 | 21 | public startRender() { 22 | this.renderStartedAt = new Date(); 23 | } 24 | 25 | public endRender() { 26 | if (!this.renderStartedAt) { 27 | return; 28 | } 29 | 30 | const timeSinceRenderStarted = new Date().getTime() - this.renderStartedAt.getTime(); 31 | this.lastRenderTimes.push(timeSinceRenderStarted); 32 | if (this.lastRenderTimes.length > 10) { 33 | this.lastRenderTimes.shift(); 34 | } 35 | this.renderStartedAt = null; 36 | } 37 | } 38 | 39 | export const updateDebouncer = new UpdateDebouncer(); 40 | -------------------------------------------------------------------------------- /src/lib/workers/markdownWorker.ts: -------------------------------------------------------------------------------- 1 | import type { WebSearchSource } from "$lib/types/WebSearch"; 2 | import { processTokens, type Token } from "$lib/utils/marked"; 3 | 4 | export type IncomingMessage = { 5 | type: "process"; 6 | content: string; 7 | sources: WebSearchSource[]; 8 | }; 9 | 10 | export type OutgoingMessage = { 11 | type: "processed"; 12 | tokens: Token[]; 13 | }; 14 | 15 | // Flag to track if the worker is currently processing a message 16 | let isProcessing = false; 17 | 18 | // Buffer to store the latest incoming message 19 | let latestMessage: IncomingMessage | null = null; 20 | 21 | // Helper function to safely handle the latest message 22 | async function processMessage() { 23 | if (latestMessage) { 24 | const nextMessage = latestMessage; 25 | 26 | latestMessage = null; 27 | isProcessing = true; 28 | 29 | try { 30 | const { content, sources } = nextMessage; 31 | const processedTokens = await processTokens(content, sources); 32 | postMessage(JSON.parse(JSON.stringify({ type: "processed", tokens: processedTokens }))); 33 | } finally { 34 | isProcessing = false; 35 | 36 | // After processing, check if a new message was buffered 37 | await new Promise((resolve) => setTimeout(resolve, 100)); 38 | processMessage(); 39 | } 40 | } 41 | } 42 | 43 | onmessage = (event) => { 44 | if (event.data.type !== "process") { 45 | return; 46 | } 47 | 48 | latestMessage = event.data as IncomingMessage; 49 | 50 | if (!isProcessing && latestMessage) { 51 | processMessage(); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
8 |
11 |

{page.status}

12 |
13 |

{page.error?.message}

14 | {#if page.error?.errorId} 15 |
16 |
{page.error
17 | 					.errorId}
18 | {/if} 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/routes/admin/stats/compute/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@sveltejs/kit"; 2 | import { logger } from "$lib/server/logger"; 3 | import { computeAllStats } from "$lib/jobs/refresh-conversation-stats"; 4 | 5 | // Triger like this: 6 | // curl -X POST "http://localhost:5173/chat/admin/stats/compute" -H "Authorization: Bearer " 7 | 8 | export async function POST() { 9 | computeAllStats().catch((e) => logger.error(e)); 10 | return json( 11 | { 12 | message: "Stats job started", 13 | }, 14 | { status: 202 } 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/api/conversation/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "$lib/server/database"; 2 | import { authCondition } from "$lib/server/auth"; 3 | import { z } from "zod"; 4 | import { models } from "$lib/server/models"; 5 | import { ObjectId } from "mongodb"; 6 | 7 | export async function GET({ locals, params }) { 8 | const id = z.string().parse(params.id); 9 | const convId = new ObjectId(id); 10 | 11 | if (locals.user?._id || locals.sessionId) { 12 | const conv = await collections.conversations.findOne({ 13 | _id: convId, 14 | ...authCondition(locals), 15 | }); 16 | 17 | if (conv) { 18 | const res = { 19 | id: conv._id, 20 | title: conv.title, 21 | updatedAt: conv.updatedAt, 22 | modelId: conv.model, 23 | assistantId: conv.assistantId, 24 | messages: conv.messages.map((message) => ({ 25 | content: message.content, 26 | from: message.from, 27 | id: message.id, 28 | createdAt: message.createdAt, 29 | updatedAt: message.updatedAt, 30 | webSearch: message.webSearch, 31 | files: message.files, 32 | updates: message.updates, 33 | reasoning: message.reasoning, 34 | })), 35 | modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, 36 | }; 37 | return Response.json(res); 38 | } else { 39 | return Response.json({ message: "Conversation not found" }, { status: 404 }); 40 | } 41 | } else { 42 | return Response.json({ message: "Must have session cookie" }, { status: 401 }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/api/conversation/[id]/message/[messageId]/+server.ts: -------------------------------------------------------------------------------- 1 | import { authCondition } from "$lib/server/auth"; 2 | import { collections } from "$lib/server/database"; 3 | import { error } from "@sveltejs/kit"; 4 | import { ObjectId } from "mongodb"; 5 | 6 | export async function DELETE({ locals, params }) { 7 | const messageId = params.messageId; 8 | 9 | if (!messageId || typeof messageId !== "string") { 10 | error(400, "Invalid message id"); 11 | } 12 | 13 | const conversation = await collections.conversations.findOne({ 14 | ...authCondition(locals), 15 | _id: new ObjectId(params.id), 16 | }); 17 | 18 | if (!conversation) { 19 | error(404, "Conversation not found"); 20 | } 21 | 22 | const filteredMessages = conversation.messages 23 | .filter( 24 | (message) => 25 | // not the message AND the message is not in ancestors 26 | !(message.id === messageId) && message.ancestors && !message.ancestors.includes(messageId) 27 | ) 28 | .map((message) => { 29 | // remove the message from children if it's there 30 | if (message.children && message.children.includes(messageId)) { 31 | message.children = message.children.filter((child) => child !== messageId); 32 | } 33 | return message; 34 | }); 35 | 36 | await collections.conversations.updateOne( 37 | { _id: conversation._id, ...authCondition(locals) }, 38 | { $set: { messages: filteredMessages } } 39 | ); 40 | 41 | return new Response(); 42 | } 43 | -------------------------------------------------------------------------------- /src/routes/api/models/+server.ts: -------------------------------------------------------------------------------- 1 | import { models } from "$lib/server/models"; 2 | 3 | export async function GET() { 4 | const res = models 5 | .filter((m) => m.unlisted == false) 6 | .map((model) => ({ 7 | id: model.id, 8 | name: model.name, 9 | websiteUrl: model.websiteUrl ?? "https://huggingface.co", 10 | modelUrl: model.modelUrl ?? "https://huggingface.co", 11 | tokenizer: model.tokenizer, 12 | datasetName: model.datasetName, 13 | datasetUrl: model.datasetUrl, 14 | displayName: model.displayName, 15 | description: model.description ?? "", 16 | logoUrl: model.logoUrl, 17 | promptExamples: model.promptExamples ?? [], 18 | preprompt: model.preprompt ?? "", 19 | multimodal: model.multimodal ?? false, 20 | unlisted: model.unlisted ?? false, 21 | tools: model.tools ?? false, 22 | hasInferenceAPI: model.hasInferenceAPI ?? false, 23 | })); 24 | return Response.json(res); 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/api/spaces-config/+server.ts: -------------------------------------------------------------------------------- 1 | import { config } from "$lib/server/config"; 2 | import { Client } from "@gradio/client"; 3 | 4 | export async function GET({ url }) { 5 | if (config.COMMUNITY_TOOLS !== "true") { 6 | return new Response("Community tools are not enabled", { status: 403 }); 7 | } 8 | 9 | const space = url.searchParams.get("space"); 10 | 11 | if (!space) { 12 | return new Response("Missing space", { status: 400 }); 13 | } 14 | // Extract namespace from space URL or use as-is if it's already in namespace format 15 | let namespace = null; 16 | if (space.startsWith("https://huggingface.co/spaces/")) { 17 | namespace = space.split("/").slice(-2).join("/"); 18 | } else if (space.match(/^[^/]+\/[^/]+$/)) { 19 | namespace = space; 20 | } 21 | 22 | if (!namespace) { 23 | return new Response( 24 | "Invalid space name. Specify a namespace or a full URL on huggingface.co.", 25 | { status: 400 } 26 | ); 27 | } 28 | 29 | try { 30 | const api = await (await Client.connect(namespace)).view_api(); 31 | return new Response(JSON.stringify(api), { 32 | status: 200, 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | }); 37 | } catch (e) { 38 | return new Response("Error fetching space API. Is the name correct?", { 39 | status: 400, 40 | headers: { 41 | "Content-Type": "application/json", 42 | }, 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/routes/api/user/+server.ts: -------------------------------------------------------------------------------- 1 | export async function GET({ locals }) { 2 | if (locals.user) { 3 | const res = { 4 | id: locals.user._id, 5 | username: locals.user.username, 6 | name: locals.user.name, 7 | email: locals.user.email, 8 | avatarUrl: locals.user.avatarUrl, 9 | hfUserId: locals.user.hfUserId, 10 | }; 11 | 12 | return Response.json(res); 13 | } 14 | return Response.json({ message: "Must be signed in" }, { status: 401 }); 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/api/user/assistants/+server.ts: -------------------------------------------------------------------------------- 1 | import { authCondition } from "$lib/server/auth"; 2 | import type { Conversation } from "$lib/types/Conversation"; 3 | import { collections } from "$lib/server/database"; 4 | import { ObjectId } from "mongodb"; 5 | 6 | export async function GET({ locals }) { 7 | if (locals.user?._id || locals.sessionId) { 8 | const settings = await collections.settings.findOne(authCondition(locals)); 9 | 10 | const conversations = await collections.conversations 11 | .find(authCondition(locals)) 12 | .sort({ updatedAt: -1 }) 13 | .project>({ 14 | assistantId: 1, 15 | }) 16 | .limit(300) 17 | .toArray(); 18 | 19 | const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; 20 | const userAssistantsSet = new Set(userAssistants); 21 | 22 | const assistantIds = [ 23 | ...userAssistants.map((el) => new ObjectId(el)), 24 | ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), 25 | ]; 26 | 27 | const assistants = await collections.assistants.find({ _id: { $in: assistantIds } }).toArray(); 28 | 29 | const res = assistants 30 | .filter((el) => userAssistantsSet.has(el._id.toString())) 31 | .map((el) => ({ 32 | ...el, 33 | _id: el._id.toString(), 34 | createdById: undefined, 35 | createdByMe: 36 | el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), 37 | })); 38 | 39 | return Response.json(res); 40 | } else { 41 | return Response.json({ message: "Must have session cookie" }, { status: 401 }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/routes/api/user/validate-token/+server.ts: -------------------------------------------------------------------------------- 1 | import { adminTokenManager } from "$lib/server/adminToken"; 2 | import { z } from "zod"; 3 | 4 | const validateTokenSchema = z.object({ 5 | token: z.string(), 6 | }); 7 | 8 | export const POST = async ({ request, locals }) => { 9 | const { success, data } = validateTokenSchema.safeParse(await request.json()); 10 | 11 | if (!success) { 12 | return new Response(JSON.stringify({ error: "Invalid token" }), { status: 400 }); 13 | } 14 | 15 | if (adminTokenManager.checkToken(data.token, locals.sessionId)) { 16 | return new Response(JSON.stringify({ valid: true })); 17 | } 18 | 19 | return new Response(JSON.stringify({ valid: false })); 20 | }; 21 | -------------------------------------------------------------------------------- /src/routes/assistant/[assistantId]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { collections } from "$lib/server/database"; 3 | import { redirect } from "@sveltejs/kit"; 4 | import { ObjectId } from "mongodb"; 5 | import { authCondition } from "$lib/server/auth.js"; 6 | 7 | export async function load({ params, locals }) { 8 | try { 9 | const assistant = await collections.assistants.findOne({ 10 | _id: new ObjectId(params.assistantId), 11 | }); 12 | 13 | if (!assistant) { 14 | redirect(302, `${base}`); 15 | } 16 | 17 | if (locals.user?._id ?? locals.sessionId) { 18 | await collections.settings.updateOne( 19 | authCondition(locals), 20 | { 21 | $set: { 22 | activeModel: assistant._id.toString(), 23 | updatedAt: new Date(), 24 | }, 25 | $push: { assistants: assistant._id }, 26 | $setOnInsert: { 27 | createdAt: new Date(), 28 | }, 29 | }, 30 | { 31 | upsert: true, 32 | } 33 | ); 34 | } 35 | 36 | return { 37 | assistant: JSON.parse(JSON.stringify(assistant)), 38 | }; 39 | } catch { 40 | redirect(302, `${base}`); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/routes/assistant/[assistantId]/thumbnail.png/ChatThumbnail.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | {#if avatar} 16 | avatar 17 | {/if} 18 |
19 |

20 | 21 | 22 | {@html logo} 23 | 24 | AI assistant 25 |

26 |

27 | {name} 28 |

29 |

30 | {description.slice(0, 160)} 31 | {#if description.length > 160}...{/if} 32 |

33 |
34 | Start chatting 35 |
36 |
37 |
38 | {#if createdByName} 39 |

40 | An AI assistant created by {createdByName} 41 |

42 | {/if} 43 |
44 | -------------------------------------------------------------------------------- /src/routes/conversation/[id]/message/[messageId]/vote/+server.ts: -------------------------------------------------------------------------------- 1 | import { authCondition } from "$lib/server/auth"; 2 | import { collections } from "$lib/server/database"; 3 | import { MetricsServer } from "$lib/server/metrics.js"; 4 | import { error } from "@sveltejs/kit"; 5 | import { ObjectId } from "mongodb"; 6 | import { z } from "zod"; 7 | 8 | export async function POST({ params, request, locals }) { 9 | const { score } = z 10 | .object({ 11 | score: z.number().int().min(-1).max(1), 12 | }) 13 | .parse(await request.json()); 14 | const conversationId = new ObjectId(params.id); 15 | const messageId = params.messageId; 16 | 17 | // aggregate votes per model in order to detect model performance degradation 18 | const model = await collections.conversations 19 | .findOne( 20 | { 21 | _id: conversationId, 22 | ...authCondition(locals), 23 | }, 24 | { projection: { model: 1 } } 25 | ) 26 | .then((c) => c?.model); 27 | 28 | if (model) { 29 | if (score === 1) { 30 | MetricsServer.getMetrics().model.votesPositive.inc({ model }); 31 | } else { 32 | MetricsServer.getMetrics().model.votesNegative.inc({ model }); 33 | } 34 | } 35 | 36 | const document = await collections.conversations.updateOne( 37 | { 38 | _id: conversationId, 39 | ...authCondition(locals), 40 | "messages.id": messageId, 41 | }, 42 | { 43 | ...(score !== 0 44 | ? { 45 | $set: { 46 | "messages.$.score": score, 47 | }, 48 | } 49 | : { $unset: { "messages.$.score": "" } }), 50 | } 51 | ); 52 | 53 | if (!document.matchedCount) { 54 | error(404, "Message not found"); 55 | } 56 | 57 | return new Response(); 58 | } 59 | -------------------------------------------------------------------------------- /src/routes/conversation/[id]/stop-generating/+server.ts: -------------------------------------------------------------------------------- 1 | import { authCondition } from "$lib/server/auth"; 2 | import { collections } from "$lib/server/database"; 3 | import { error } from "@sveltejs/kit"; 4 | import { ObjectId } from "mongodb"; 5 | 6 | /** 7 | * Ideally, we'd be able to detect the client-side abort, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 8 | */ 9 | export async function POST({ params, locals }) { 10 | const conversationId = new ObjectId(params.id); 11 | 12 | const conversation = await collections.conversations.findOne({ 13 | _id: conversationId, 14 | ...authCondition(locals), 15 | }); 16 | 17 | if (!conversation) { 18 | error(404, "Conversation not found"); 19 | } 20 | 21 | await collections.abortedGenerations.updateOne( 22 | { conversationId }, 23 | { $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, 24 | { upsert: true } 25 | ); 26 | 27 | return new Response(); 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/healthcheck/+server.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | return new Response("OK", { status: 200 }); 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/login/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@sveltejs/kit"; 2 | import { getOIDCAuthorizationUrl } from "$lib/server/auth"; 3 | import { base } from "$app/paths"; 4 | import { config } from "$lib/server/config"; 5 | 6 | export const actions = { 7 | async default({ url, locals, request }) { 8 | const referer = request.headers.get("referer"); 9 | let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`; 10 | 11 | // TODO: Handle errors if provider is not responding 12 | 13 | if (url.searchParams.has("callback")) { 14 | const callback = url.searchParams.get("callback") || redirectURI; 15 | if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) { 16 | redirectURI = callback; 17 | } 18 | } 19 | 20 | const authorizationUrl = await getOIDCAuthorizationUrl( 21 | { redirectURI }, 22 | { sessionId: locals.sessionId } 23 | ); 24 | 25 | redirect(303, authorizationUrl); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/routes/logout/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | import { base } from "$app/paths"; 3 | import { config } from "$lib/server/config"; 4 | import { collections } from "$lib/server/database"; 5 | import { redirect } from "@sveltejs/kit"; 6 | 7 | export const actions = { 8 | async default({ cookies, locals }) { 9 | await collections.sessions.deleteOne({ sessionId: locals.sessionId }); 10 | 11 | cookies.delete(config.COOKIE_NAME, { 12 | path: "/", 13 | // So that it works inside the space's iframe 14 | sameSite: dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", 15 | secure: !dev && !(config.ALLOW_INSECURE_COOKIES === "true"), 16 | httpOnly: true, 17 | }); 18 | redirect(303, `${base}/`); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/routes/models/[...model]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { authCondition } from "$lib/server/auth.js"; 3 | import { collections } from "$lib/server/database"; 4 | import { models } from "$lib/server/models"; 5 | import { redirect } from "@sveltejs/kit"; 6 | 7 | export async function load({ params, locals, parent }) { 8 | const model = models.find(({ id }) => id === params.model); 9 | const data = await parent(); 10 | 11 | if (!model || model.unlisted) { 12 | redirect(302, `${base}/`); 13 | } 14 | 15 | if (locals.user?._id ?? locals.sessionId) { 16 | await collections.settings.updateOne( 17 | authCondition(locals), 18 | { 19 | $set: { 20 | activeModel: model.id, 21 | updatedAt: new Date(), 22 | }, 23 | $setOnInsert: { 24 | createdAt: new Date(), 25 | }, 26 | }, 27 | { 28 | upsert: true, 29 | } 30 | ); 31 | } 32 | 33 | return { 34 | settings: { 35 | ...data.settings, 36 | activeModel: model.id, 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/models/[...model]/thumbnail.png/+server.ts: -------------------------------------------------------------------------------- 1 | import ModelThumbnail from "./ModelThumbnail.svelte"; 2 | import { redirect, type RequestHandler } from "@sveltejs/kit"; 3 | 4 | import { Resvg } from "@resvg/resvg-js"; 5 | import satori from "satori"; 6 | import { html } from "satori-html"; 7 | 8 | import InterRegular from "$lib/server/fonts/Inter-Regular.ttf"; 9 | import InterBold from "$lib/server/fonts/Inter-Bold.ttf"; 10 | import { base } from "$app/paths"; 11 | import { models } from "$lib/server/models"; 12 | import { render } from "svelte/server"; 13 | 14 | export const GET: RequestHandler = (async ({ params }) => { 15 | const model = models.find(({ id }) => id === params.model); 16 | 17 | if (!model || model.unlisted) { 18 | redirect(302, `${base}/`); 19 | } 20 | const renderedComponent = render(ModelThumbnail, { 21 | props: { 22 | name: model.name, 23 | logoUrl: model.logoUrl, 24 | }, 25 | }); 26 | 27 | const reactLike = html("" + renderedComponent.body); 28 | 29 | const svg = await satori(reactLike, { 30 | width: 1200, 31 | height: 648, 32 | fonts: [ 33 | { 34 | name: "Inter", 35 | data: InterRegular as unknown as ArrayBuffer, 36 | weight: 500, 37 | }, 38 | { 39 | name: "Inter", 40 | data: InterBold as unknown as ArrayBuffer, 41 | weight: 700, 42 | }, 43 | ], 44 | }); 45 | 46 | const png = new Resvg(svg, { 47 | fitTo: { mode: "original" }, 48 | }) 49 | .render() 50 | .asPng(); 51 | 52 | return new Response(png, { 53 | headers: { 54 | "Content-Type": "image/png", 55 | }, 56 | }); 57 | }) satisfies RequestHandler; 58 | -------------------------------------------------------------------------------- /src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | {#if logoUrl} 16 | avatar 17 | {/if} 18 |

19 | {name} 20 |

21 |
22 | 23 |
27 | Try it now 28 | {#if publicConfig.isHuggingChat} 29 | on 30 | {/if} 31 | 32 | {#if publicConfig.isHuggingChat} 33 |
34 | 38 | HuggingChat 39 |
40 | {/if} 41 |
42 |
43 | -------------------------------------------------------------------------------- /src/routes/privacy/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 | {@html marked(privacy, { gfm: true })} 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/routes/r/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect, type LoadEvent } from "@sveltejs/kit"; 2 | 3 | export const load = async ({ params, url }: LoadEvent) => { 4 | const leafId = url.searchParams.get("leafId"); 5 | 6 | redirect(302, "../conversation/" + params.id + `?leafId=${leafId}`); 7 | }; 8 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/src/routes/settings/(nav)/+page.svelte -------------------------------------------------------------------------------- /src/routes/settings/(nav)/[...model]/+page.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { redirect } from "@sveltejs/kit"; 3 | 4 | export async function load({ parent, params }) { 5 | const data = await parent(); 6 | 7 | const model = data.models.find((m: { id: string }) => m.id === params.model); 8 | 9 | if (!model || model.unlisted) { 10 | redirect(302, `${base}/settings`); 11 | } 12 | 13 | return data; 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/assistants/[assistantId]/+page.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { redirect } from "@sveltejs/kit"; 3 | 4 | export async function load({ parent, params }) { 5 | const data = await parent(); 6 | 7 | const assistant = data.settings.assistants.find((id) => id === params.assistantId); 8 | 9 | if (!assistant) { 10 | redirect(302, `${base}/assistant/${params.assistantId}`); 11 | } 12 | 13 | return data; 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/assistants/[assistantId]/avatar.jpg/+server.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "$lib/server/database"; 2 | import { error, type RequestHandler } from "@sveltejs/kit"; 3 | import { ObjectId } from "mongodb"; 4 | 5 | export const GET: RequestHandler = async ({ params }) => { 6 | const assistant = await collections.assistants.findOne({ 7 | _id: new ObjectId(params.assistantId), 8 | }); 9 | 10 | if (!assistant) { 11 | error(404, "No assistant found"); 12 | } 13 | 14 | if (!assistant.avatar) { 15 | error(404, "No avatar found"); 16 | } 17 | 18 | const fileId = collections.bucket.find({ filename: assistant._id.toString() }); 19 | 20 | const content = await fileId.next().then(async (file) => { 21 | if (!file?._id) { 22 | error(404, "Avatar not found"); 23 | } 24 | 25 | const fileStream = collections.bucket.openDownloadStream(file?._id); 26 | 27 | const fileBuffer = await new Promise((resolve, reject) => { 28 | const chunks: Uint8Array[] = []; 29 | fileStream.on("data", (chunk) => chunks.push(chunk)); 30 | fileStream.on("error", reject); 31 | fileStream.on("end", () => resolve(Buffer.concat(chunks))); 32 | }); 33 | 34 | return fileBuffer; 35 | }); 36 | 37 | return new Response(content, { 38 | headers: { 39 | "Content-Type": "image/jpeg", 40 | "Content-Security-Policy": 41 | "default-src 'none'; script-src 'none'; style-src 'none'; sandbox;", 42 | }, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/routes/settings/(nav)/assistants/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/settings/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { collections } from "$lib/server/database"; 2 | import type { LayoutServerLoad } from "./$types"; 3 | import type { Report } from "$lib/types/Report"; 4 | 5 | export const load = (async ({ locals, parent }) => { 6 | const { assistants } = await parent(); 7 | 8 | let reportsByUser: string[] = []; 9 | const createdBy = locals.user?._id ?? locals.sessionId; 10 | if (createdBy) { 11 | const reports = await collections.reports 12 | .find< 13 | Pick 14 | >({ createdBy, object: "assistant" }, { projection: { _id: 0, contentId: 1 } }) 15 | .toArray(); 16 | reportsByUser = reports.map((r) => r.contentId.toString()); 17 | } 18 | 19 | return { 20 | assistants: (await assistants).map((el) => ({ 21 | ...el, 22 | reported: reportsByUser.includes(el._id), 23 | })), 24 | }; 25 | }) satisfies LayoutServerLoad; 26 | -------------------------------------------------------------------------------- /src/routes/settings/+layout.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | goto(previousPage)} 28 | width="h-[95dvh] w-[90dvw] pb-0 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[95dvh] xl:w-[1200px] 2xl:h-[75dvh]" 29 | > 30 | {@render children?.()} 31 | {#if $settings.recentlySaved} 32 |
35 | 36 | Saved 37 |
38 | {/if} 39 |
40 | -------------------------------------------------------------------------------- /src/routes/tools/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | {#if publicConfig.isHuggingChat} 14 | HuggingChat - Tools 15 | 16 | 17 | 18 | 23 | 24 | {/if} 25 | 26 | 27 | {@render children?.()} 28 | -------------------------------------------------------------------------------- /src/routes/tools/+layout.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { redirect } from "@sveltejs/kit"; 3 | 4 | export async function load({ parent }) { 5 | const { enableCommunityTools } = await parent(); 6 | 7 | if (enableCommunityTools) { 8 | return {}; 9 | } 10 | 11 | redirect(302, `${base}/`); 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/tools/[toolId]/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { base } from "$app/paths"; 2 | import { collections } from "$lib/server/database.js"; 3 | import { toolFromConfigs } from "$lib/server/tools/index.js"; 4 | import { ReviewStatus } from "$lib/types/Review.js"; 5 | import { redirect } from "@sveltejs/kit"; 6 | import { ObjectId } from "mongodb"; 7 | 8 | export const load = async ({ params, locals }) => { 9 | const tool = await collections.tools.findOne({ _id: new ObjectId(params.toolId) }); 10 | 11 | if (!tool) { 12 | const tool = toolFromConfigs.find((el) => el._id.toString() === params.toolId); 13 | if (!tool) { 14 | redirect(302, `${base}/tools`); 15 | } 16 | return { 17 | tool: { 18 | ...tool, 19 | _id: tool._id.toString(), 20 | call: undefined, 21 | createdById: null, 22 | createdByName: null, 23 | createdByMe: false, 24 | reported: false, 25 | review: ReviewStatus.APPROVED, 26 | }, 27 | }; 28 | } 29 | 30 | const reported = await collections.reports.findOne({ 31 | contentId: tool._id, 32 | object: "tool", 33 | }); 34 | 35 | return { 36 | tool: { 37 | ...tool, 38 | _id: tool._id.toString(), 39 | call: undefined, 40 | createdById: tool.createdById.toString(), 41 | createdByMe: 42 | tool.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), 43 | reported: !!reported, 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/routes/tools/[toolId]/edit/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | window.history.back()} 10 | width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]" 11 | closeButton 12 | > 13 | { 17 | window.history.back(); 18 | }} 19 | /> 20 | 21 | -------------------------------------------------------------------------------- /src/routes/tools/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | window.history.back()} 8 | width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]" 9 | > 10 | window.history.back()} /> 11 | 12 | -------------------------------------------------------------------------------- /src/styles/highlight-js.css: -------------------------------------------------------------------------------- 1 | @import "highlight.js/styles/atom-one-dark"; 2 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "./highlight-js.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer components { 8 | .btn { 9 | @apply inline-flex flex-shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap outline-none transition-all focus:ring disabled:cursor-default; 10 | } 11 | 12 | .active-model { 13 | @apply border-blue-500 bg-blue-500/5 hover:bg-blue-500/10; 14 | } 15 | 16 | .file-hoverable { 17 | @apply hover:bg-gray-500/10; 18 | } 19 | 20 | .base-tool { 21 | @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; 22 | } 23 | 24 | .active-tool { 25 | @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; 26 | } 27 | } 28 | 29 | @layer utilities { 30 | .scrollbar-custom { 31 | @apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/10 scrollbar-thumb-rounded-full scrollbar-w-1 hover:scrollbar-thumb-black/20 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20; 32 | } 33 | } 34 | 35 | .katex-display { 36 | overflow: auto hidden; 37 | } 38 | -------------------------------------------------------------------------------- /static/chatui/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/chatui/apple-touch-icon.png -------------------------------------------------------------------------------- /static/chatui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/chatui/favicon.ico -------------------------------------------------------------------------------- /static/chatui/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/chatui/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/chatui/icon-128x128.png -------------------------------------------------------------------------------- /static/chatui/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/chatui/icon-256x256.png -------------------------------------------------------------------------------- /static/chatui/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/chatui/icon-512x512.png -------------------------------------------------------------------------------- /static/chatui/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/chatui/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /static/chatui/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#ffffff", 3 | "name": "Chat UI", 4 | "short_name": "Chat UI", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "icons": [ 8 | { 9 | "src": "/chatui/icon-128x128.png", 10 | "sizes": "128x128", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "/chatui/icon-256x256.png", 15 | "sizes": "256x256", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "/chatui/icon-512x512.png", 20 | "sizes": "512x512", 21 | "type": "image/png" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /static/huggingchat/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/apple-touch-icon.png -------------------------------------------------------------------------------- /static/huggingchat/assistants-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/assistants-thumbnail.png -------------------------------------------------------------------------------- /static/huggingchat/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/favicon.ico -------------------------------------------------------------------------------- /static/huggingchat/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-128x128.png -------------------------------------------------------------------------------- /static/huggingchat/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-144x144.png -------------------------------------------------------------------------------- /static/huggingchat/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-192x192.png -------------------------------------------------------------------------------- /static/huggingchat/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-256x256.png -------------------------------------------------------------------------------- /static/huggingchat/icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-36x36.png -------------------------------------------------------------------------------- /static/huggingchat/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-48x48.png -------------------------------------------------------------------------------- /static/huggingchat/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-512x512.png -------------------------------------------------------------------------------- /static/huggingchat/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-72x72.png -------------------------------------------------------------------------------- /static/huggingchat/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/icon-96x96.png -------------------------------------------------------------------------------- /static/huggingchat/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /static/huggingchat/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#ffffff", 3 | "name": "HuggingChat", 4 | "short_name": "HuggingChat", 5 | "display": "standalone", 6 | "start_url": "/chat", 7 | "icons": [ 8 | { 9 | "src": "/chat/huggingchat/icon-36x36.png", 10 | "sizes": "36x36", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "/chat/huggingchat/icon-48x48.png", 15 | "sizes": "48x48", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "/chat/huggingchat/icon-72x72.png", 20 | "sizes": "72x72", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "/chat/huggingchat/icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image/png" 27 | }, 28 | { 29 | "src": "/chat/huggingchat/icon-128x128.png", 30 | "sizes": "128x128", 31 | "type": "image/png" 32 | }, 33 | { 34 | "src": "/chat/huggingchat/icon-144x144.png", 35 | "sizes": "144x144", 36 | "type": "image/png" 37 | }, 38 | { 39 | "src": "/chat/huggingchat/icon-192x192.png", 40 | "sizes": "192x192", 41 | "type": "image/png" 42 | }, 43 | { 44 | "src": "/chat/huggingchat/icon-256x256.png", 45 | "sizes": "256x256", 46 | "type": "image/png" 47 | }, 48 | { 49 | "src": "/chat/huggingchat/icon-512x512.png", 50 | "sizes": "512x512", 51 | "type": "image/png" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /static/huggingchat/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/thumbnail.png -------------------------------------------------------------------------------- /static/huggingchat/tools-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/static/huggingchat/tools-thumbnail.png -------------------------------------------------------------------------------- /stub/@reflink/reflink/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huggingface/chat-ui/21dd9683d3e2670779ff974adf6e109880d414a6/stub/@reflink/reflink/index.js -------------------------------------------------------------------------------- /stub/@reflink/reflink/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reflink/reflink", 3 | "version": "0.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-node"; 2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 3 | import dotenv from "dotenv"; 4 | import { execSync } from "child_process"; 5 | 6 | dotenv.config({ path: "./.env.local" }); 7 | dotenv.config({ path: "./.env" }); 8 | 9 | function getCurrentCommitSHA() { 10 | try { 11 | return execSync("git rev-parse HEAD").toString(); 12 | } catch (error) { 13 | console.error("Error getting current commit SHA:", error); 14 | return "unknown"; 15 | } 16 | } 17 | 18 | process.env.PUBLIC_VERSION ??= process.env.npm_package_version; 19 | process.env.PUBLIC_COMMIT_SHA ??= getCurrentCommitSHA(); 20 | 21 | /** @type {import('@sveltejs/kit').Config} */ 22 | const config = { 23 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 24 | // for more information about preprocessors 25 | preprocess: vitePreprocess(), 26 | 27 | kit: { 28 | adapter: adapter(), 29 | 30 | paths: { 31 | base: process.env.APP_BASE || "", 32 | relative: false, 33 | }, 34 | csrf: { 35 | // handled in hooks.server.ts, because we can have multiple valid origins 36 | checkOrigin: false, 37 | }, 38 | csp: { 39 | directives: { 40 | ...(process.env.ALLOW_IFRAME === "true" ? {} : { "frame-ancestors": ["'none'"] }), 41 | }, 42 | }, 43 | }, 44 | }; 45 | 46 | export default config; 47 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | const colors = require("tailwindcss/colors"); 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | darkMode: "class", 7 | mode: "jit", 8 | content: ["./src/**/*.{html,js,svelte,ts}"], 9 | theme: { 10 | extend: { 11 | colors: { 12 | primary: colors[process.env.PUBLIC_APP_COLOR], 13 | }, 14 | fontSize: { 15 | xxs: "0.625rem", 16 | smd: "0.94rem", 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | require("tailwind-scrollbar")({ nocompatible: true }), 22 | require("@tailwindcss/typography"), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ES2018" 13 | }, 14 | "exclude": ["vite.config.ts"] 15 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | --------------------------------------------------------------------------------