Implement v2 chat layout designs (#5074)

* New chat history layout with chat bubbles (#4985)

* new chat history layout, remove message alignment setting

* remove orphaned chat alignment hook and MessageDirection

* remove workspace profile picture setting and fetch

* clean up unnecessary changes

* add light mode colors to chat ui and main page backgrounds

* update chat message and action icon colors for light mode

* update thinking and agent ui, layout, sizing

* update user message uploaded images ui

* update thought, agent containers to use new colors

* add truncatable content with gradient to user chat messages

* fix citations margin

* implement new edit message UI with save and submit actions

* add translations for TruncatableContent subcomponent

* remove unused props

* fix text colors for default mode chats, agent, thoughts container

* Normalize translations for new chat history layout (#5022)

* normalize translations

* update translations with DMR

* lint

* fix mismatched home container colors

* fix: add password character validation to onboarding single-user setup (#5037)

* fix single user mode password bug

* share const

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Native Tool calling (#5071)

* checkpoint

* test MCP and flows

* add native tool call detection back to LMStudio

* add native tool call loops for Ollama

* Add ablity detection to DMR (regex parse)

* bedrock and generic openai with ENV flag

* deepseek native tool calling

* localAI native function

* groq support

* linting, add litellm and OR native tool calling via flag

* fix: resolve Gemini agent 400 error on tool call responses (#5054)

* add gtc__ prefix to tool call names in Gemini agent message formatting

* resolve Gemini agent 400 error on tool call responses

* add comments explaining geminis thought signatures

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* fix: prevent CMD/CTRL+Arrow scroll from overriding textarea cursor movement (#5053)

prevent CMD/CTRL+Arrow scroll from overriding textarea cursor movement

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* linting, assistant speaker spacing and order, copy/edit order

---------

Co-authored-by: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com>
Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* Implement new citations UI (#5038)

* new chat history layout, remove message alignment setting

* remove orphaned chat alignment hook and MessageDirection

* remove workspace profile picture setting and fetch

* clean up unnecessary changes

* add light mode colors to chat ui and main page backgrounds

* update chat message and action icon colors for light mode

* update thinking and agent ui, layout, sizing

* update user message uploaded images ui

* update thought, agent containers to use new colors

* add truncatable content with gradient to user chat messages

* fix citations margin

* implement new edit message UI with save and submit actions

* add translations for TruncatableContent subcomponent

* remove unused props

* fix text colors for default mode chats, agent, thoughts container

* Normalize translations for new chat history layout (#5022)

* normalize translations

* update translations with DMR

* lint

* fix mismatched home container colors

* implement new citations ui with sources sidebar

* bottom sheet for mobile citations

* convert mobile citations bottom sheet to new modal design

* add score, border separators for mobile citations modal

* push down sources sidebar in password/multiuser mode

* fix animation gap, simplify sources sidebar by splitting state to persist data on animation

* add english translations

* fix spacing from citations sidebar when user has auth

* Normalize translations for new citation UI (#5087)

* normalize translations

* update translations using DMR

* fix pluralize to use i18n native solution
change reset to immediate clear
fix spacing for TTS when showing or not to not have space

* proper pluralize

* hide metrics on mobile, fix last message padding on mobile

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* New prompt input ui/tools menu (#5070)

* wip new prompt input ui/tools menu

* fix colors for prompt input

* redesign workspace llm selector, extract text size + model picker to components

* refactor ToolsMenu component

* fix colors/refactor WorkspaceModelPicker

* fix spacing in ws model picker, change order of tools menu tabs

* fix slash commands showing /reset instead of /exit during active agent session

* refactor ToolsMenu to be much simpler

* cleanup, fix behavior of setupup provider in WorkspaceModelPicker

* simplify AgentSkillsTab toggle logic

* add english translations for new components

* remove legacy slash command/agent popups, add ToolsMenu keyboard nav

* fix spacing of workspace model picker text

* fix SourcesSidebar and TextSizeMenu positioning after merge

* fix keyboard nav in ToolsMenu when clicking on tools button to open

* typo

* only auto pop up tools menu when prompt input is empty with /

* fix z index for tools menu on citation

* fix behavior of / in prompt input

* move global window agent session state to module level variable

* fix prompt input not clearing on /reset

* missing translations

* revert translating slash command

* fix STT auto-submit not working on home page

* Normalize translations for new prompt input/tools menu UI (#5130)

* normalize translations

* update translations using DMR script

* normalize translations

* update translations using DMR script

* remove slash_exit

* fix skills.js import after merge

* fix tooltip z-index rendering behind citations

* patch translation prune script to not remove special cases

* updates to tools input

* factory translations

* use safeJsonParse in clearPromptInputDraft

* normalize translations

* disable agent skill toggles during active agent sessions + show tooltip on disabled

* normalize translations

* handle enter key behavior when tools menu is open

* fix unfocusable modal for slash command edit/new

* fix sending prompt when editing/creating slash commands

* hide/show agent skills in tools menu based on role

* container borders for dark/light mode compliance to designs

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* update how tooltip works for agent menu

* update prompt input to show agent button with CTA in agent panel for user clarify
update agent session start prompt button in input

* translations

* translations + move regex for slash commands to constants

* fix open sidebar ux

* fix tools menu to always open to slash commands, dismiss auto pop up

* fix sidebar open/close button overlapping with ws model picker

---------

Co-authored-by: Sean Hatfield <seanhatfield5@gmail.com>
Co-authored-by: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com>
This commit is contained in:
Timothy Carambat 2026-03-10 12:50:19 -07:00 committed by GitHub
parent 868358597e
commit 21ac874cfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 3107 additions and 2199 deletions

View File

@ -3,6 +3,7 @@ import { SidebarSimple } from "@phosphor-icons/react";
import paths from "@/utils/paths";
import { Tooltip } from "react-tooltip";
const SIDEBAR_TOGGLE_STORAGE_KEY = "anythingllm_sidebar_toggle";
export const SIDEBAR_TOGGLE_EVENT = "sidebar-toggle";
/**
* Returns the previous state of the sidebar from localStorage.
@ -62,6 +63,11 @@ export function useSidebarToggle() {
SIDEBAR_TOGGLE_STORAGE_KEY,
showSidebar ? "open" : "closed"
);
window.dispatchEvent(
new CustomEvent(SIDEBAR_TOGGLE_EVENT, {
detail: { open: showSidebar },
})
);
}, [showSidebar]);
return { showSidebar, setShowSidebar, canToggleSidebar };

View File

@ -31,7 +31,6 @@ import CustomCell from "./CustomCell.jsx";
import Tooltip from "./CustomTooltip.jsx";
import { safeJsonParse } from "@/utils/request.js";
import renderMarkdown from "@/utils/chat/markdown.js";
import { WorkspaceProfileImage } from "../PromptReply/index.jsx";
import { memo, useCallback, useState } from "react";
import { saveAs } from "file-saver";
import { useGenerateImage } from "recharts-to-png";
@ -41,7 +40,7 @@ const dataFormatter = (number) => {
return Intl.NumberFormat("us").format(number).toString();
};
export function Chartable({ props, workspace }) {
export function Chartable({ props }) {
const [getDivJpeg, { ref }] = useGenerateImage({
quality: 1,
type: "image/jpeg",
@ -387,20 +386,17 @@ export function Chartable({ props, workspace }) {
if (!!props.chatId) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="relative w-full">
<DownloadGraph onClick={handleDownload} />
<div ref={ref}>{renderChart()}</div>
<span
className={`flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{
__html: renderMarkdown(content.caption),
}}
/>
</div>
<div className="flex justify-start w-full">
<div className="py-2 px-4 w-full flex flex-col md:max-w-[80%]">
<div className="relative w-full">
<DownloadGraph onClick={handleDownload} />
<div ref={ref}>{renderChart()}</div>
<span
className="flex flex-col gap-y-1 mt-2"
dangerouslySetInnerHTML={{
__html: renderMarkdown(content.caption),
}}
/>
</div>
</div>
</div>
@ -408,20 +404,18 @@ export function Chartable({ props, workspace }) {
}
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex justify-start w-full">
<div className="py-2 px-4 w-full flex flex-col md:max-w-[80%]">
<div className="relative w-full">
<DownloadGraph onClick={handleDownload} />
<div ref={ref}>{renderChart()}</div>
</div>
<div className="flex gap-x-5">
<span
className={`flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{
__html: renderMarkdown(content.caption),
}}
/>
</div>
<span
className="flex flex-col gap-y-1 mt-2"
dangerouslySetInnerHTML={{
__html: renderMarkdown(content.caption),
}}
/>
</div>
</div>
);

View File

@ -1,10 +1,8 @@
import { Fragment, useState } from "react";
import { Fragment } from "react";
import { decode as HTMLDecode } from "he";
import truncate from "truncate";
import ModalWrapper from "@/components/ModalWrapper";
import { middleTruncate } from "@/utils/directories";
import {
CaretRight,
FileText,
Info,
ArrowSquareOut,
@ -14,16 +12,41 @@ import {
LinkSimple,
GitlabLogo,
} from "@phosphor-icons/react";
import ConfluenceLogo from "@/media/dataConnectors/confluence.png";
import DrupalWikiLogo from "@/media/dataConnectors/drupalwiki.png";
import ObsidianLogo from "@/media/dataConnectors/obsidian.png";
import PaperlessNgxLogo from "@/media/dataConnectors/paperlessngx.png";
import { toPercentString } from "@/utils/numbers";
import { useTranslation } from "react-i18next";
import pluralize from "pluralize";
import useTextSize from "@/hooks/useTextSize";
import { useSourcesSidebar } from "../../SourcesSidebar";
function combineLikeSources(sources) {
const CIRCLE_ICONS = {
file: FileText,
link: LinkSimple,
youtube: YoutubeLogo,
github: GithubLogo,
gitlab: GitlabLogo,
confluence: LinkSimple,
drupalwiki: FileText,
obsidian: FileText,
paperlessNgx: FileText,
};
/**
* Renders a circle with a source type icon inside.
* @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type
* @param {number} [props.size] - Circle diameter in px
* @param {number} [props.iconSize] - Icon size in px
*/
export function SourceTypeCircle({ type = "file", size = 22, iconSize = 12 }) {
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
return (
<div
className="bg-white light:bg-slate-100 rounded-full flex items-center justify-center"
style={{ width: size, height: size }}
>
<Icon size={iconSize} weight="bold" className="text-black" />
</div>
);
}
export function combineLikeSources(sources) {
const combined = {};
sources.forEach((source) => {
const { id, title, text, chunkSource = "", score = null } = source;
@ -42,106 +65,83 @@ function combineLikeSources(sources) {
}
export default function Citations({ sources = [] }) {
const [open, setOpen] = useState(false);
const [selectedSource, setSelectedSource] = useState(null);
const {
sidebarOpen,
openSidebar,
closeSidebar,
sources: currentSources,
} = useSourcesSidebar();
const { t } = useTranslation();
const { textSizeClass } = useTextSize();
if (sources.length === 0) return null;
return (
<div className="flex flex-col mt-4 justify-left">
<button
onClick={() => setOpen(!open)}
className={`border-none font-semibold text-white/50 light:text-black/50 font-medium italic ${textSizeClass} text-left ml-14 pt-2 ${
open ? "pb-2" : ""
} hover:text-white/75 hover:light:text-black/75 transition-all duration-300`}
>
{open
? t("chat_window.hide_citations")
: t("chat_window.show_citations")}
<CaretRight
weight="bold"
size={14}
className={`inline-block ml-1 transform transition-transform duration-300 ${
open ? "rotate-90" : ""
}`}
/>
</button>
{open && (
<div className="flex flex-wrap flex-col items-start overflow-x-scroll no-scroll mt-1 ml-14 gap-y-2">
{combineLikeSources(sources).map((source, idx) => (
<Citation
key={source.title || idx.toString()}
source={source}
onClick={() => setSelectedSource(source)}
textSizeClass={textSizeClass}
/>
))}
</div>
)}
{selectedSource && (
<CitationDetailModal
source={selectedSource}
onClose={() => setSelectedSource(null)}
/>
)}
</div>
);
}
const combined = combineLikeSources(sources);
const visibleSources = combined.slice(0, 3);
const remainingCount = Math.max(0, combined.length - 3);
const Citation = ({ source, onClick, textSizeClass }) => {
const { title, references = 1 } = source;
if (!title) return null;
const chunkSourceInfo = parseChunkSource(source);
const truncatedTitle = chunkSourceInfo?.text ?? middleTruncate(title, 25);
const CitationIcon = ICONS.hasOwnProperty(chunkSourceInfo?.icon)
? ICONS[chunkSourceInfo.icon]
: ICONS.file;
function handleOpenSourcesSidebar() {
if (sidebarOpen && sources === currentSources) {
closeSidebar();
} else {
openSidebar(sources);
}
}
return (
<button
className={`flex doc__source gap-x-1 ${textSizeClass}`}
onClick={onClick}
onClick={handleOpenSourcesSidebar}
className="w-fit flex items-center gap-[5px] px-[10px] py-[4px] rounded-full hover:bg-white/5 light:hover:bg-black/5 transition-colors"
type="button"
>
<div className="flex items-start flex-1 pt-[4px]">
<CitationIcon size={16} />
</div>
<div className="flex flex-col items-start gap-y-[0.2px] px-1">
<p
className={`!m-0 font-semibold whitespace-nowrap text-theme-text-primary hover:opacity-55 ${textSizeClass}`}
>
{truncatedTitle}
</p>
<p
className={`!m-0 text-[10px] font-medium text-theme-text-secondary ${textSizeClass}`}
>{`${references} ${pluralize("Reference", Number(references) || 1)}`}</p>
<span className="text-xs text-white light:text-slate-800">
{t("chat_window.sources")}
</span>
<div
className="relative h-[22px]"
style={{ width: `${visibleSources.length * 17 + 5}px` }}
>
{visibleSources.map((source, idx) => {
const info = parseChunkSource(source);
return (
<div
key={source.title || idx}
className="absolute top-0 size-[22px] rounded-full border-2 border-zinc-800 light:border-white"
style={{ left: `${idx * 17}px`, zIndex: 3 - idx }}
>
<SourceTypeCircle type={info.icon} size={18} iconSize={10} />
</div>
);
})}
</div>
{remainingCount > 0 && (
<span className="text-xs text-white light:text-slate-800">
+ {remainingCount}
</span>
)}
</button>
);
};
}
function omitChunkHeader(text) {
export function omitChunkHeader(text) {
if (!text.includes("<document_metadata>")) return text;
return text.split("</document_metadata>")[1].trim();
}
function CitationDetailModal({ source, onClose }) {
export function CitationDetailModal({ source, onClose }) {
const { references, title, chunks } = source;
const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);
const { t } = useTranslation();
return (
<ModalWrapper isOpen={!!source}>
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
<div className="w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow border-2 border-zinc-700 light:border-slate-300 overflow-hidden">
<div className="relative p-6 border-b rounded-t border-zinc-700 light:border-slate-300">
<div className="w-full flex gap-x-2 items-center">
{isUrl ? (
<a
href={linkTo}
target="_blank"
rel="noreferrer"
className="text-xl w-[90%] font-semibold text-white whitespace-nowrap hover:underline hover:text-blue-300 flex items-center gap-x-1"
className="text-xl w-[90%] font-semibold text-white light:text-slate-900 whitespace-nowrap hover:underline hover:text-blue-300 light:hover:text-blue-600 flex items-center gap-x-1"
>
<div className="flex items-center gap-x-1 max-w-full overflow-hidden">
<h3 className="truncate text-ellipsis whitespace-nowrap overflow-hidden w-full">
@ -151,22 +151,26 @@ function CitationDetailModal({ source, onClose }) {
</div>
</a>
) : (
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
<h3 className="text-xl font-semibold text-white light:text-slate-900 overflow-hidden overflow-ellipsis whitespace-nowrap">
{truncate(title, 45)}
</h3>
)}
</div>
{references > 1 && (
<p className="text-xs text-gray-400 mt-2">
<p className="text-xs text-zinc-400 light:text-slate-500 mt-2">
Referenced {references} times.
</p>
)}
<button
onClick={onClose}
type="button"
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-zinc-700 light:hover:bg-slate-200 border-transparent border"
>
<X size={24} weight="bold" className="text-white" />
<X
size={24}
weight="bold"
className="text-white light:text-slate-900"
/>
</button>
</div>
<div
@ -176,28 +180,31 @@ function CitationDetailModal({ source, onClose }) {
<div className="py-7 px-9 space-y-2 flex-col">
{chunks.map(({ text, score }, idx) => (
<Fragment key={idx}>
<div className="pt-6 text-white">
<div className="pt-6 text-white light:text-slate-900">
<div className="flex flex-col w-full justify-start pb-6 gap-y-1">
<p className="text-white whitespace-pre-line">
<p className="text-white light:text-slate-900 whitespace-pre-line">
{HTMLDecode(omitChunkHeader(text))}
</p>
{!!score && (
<div className="w-full flex items-center text-xs text-white/60 gap-x-2 cursor-default">
<div className="w-full flex items-center text-xs text-white/60 light:text-slate-500 gap-x-2 cursor-default">
<div
data-tooltip-id="similarity-score"
data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}
className="flex items-center gap-x-1"
>
<Info size={14} />
<p>{toPercentString(score)} match</p>
<p>
{toPercentString(score)}{" "}
{t("chat_window.similarity_match")}
</p>
</div>
</div>
)}
</div>
</div>
{idx !== chunks.length - 1 && (
<hr className="border-theme-modal-border" />
<hr className="border-zinc-700 light:border-slate-300" />
)}
</Fragment>
))}
@ -228,7 +235,7 @@ const supportedSources = [
* @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options
* @returns {{isUrl: boolean, text: string, href: string, icon: string}}
*/
function parseChunkSource({ title = "", chunks = [] }) {
export function parseChunkSource({ title = "", chunks = [] }) {
const nullResponse = {
isUrl: false,
text: null,
@ -315,33 +322,3 @@ function parseChunkSource({ title = "", chunks = [] }) {
}
return nullResponse;
}
const ConfluenceIcon = ({ size = 16, ...props }) => (
<img src={ConfluenceLogo} {...props} width={size} height={size} />
);
const DrupalWikiIcon = ({ size = 16, ...props }) => (
<img src={DrupalWikiLogo} {...props} width={size} height={size} />
);
const ObsidianIcon = ({ size = 16, ...props }) => (
<img src={ObsidianLogo} {...props} width={size} height={size} />
);
const PaperlessNgxIcon = ({ size = 16, ...props }) => (
<img
src={PaperlessNgxLogo}
{...props}
width={size}
height={size}
className="rounded-sm bg-white"
/>
);
const ICONS = {
file: FileText,
link: LinkSimple,
youtube: YoutubeLogo,
github: GithubLogo,
gitlab: GitlabLogo,
confluence: ConfluenceIcon,
drupalwiki: DrupalWikiIcon,
obsidian: ObsidianIcon,
paperlessNgx: PaperlessNgxIcon,
};

View File

@ -40,7 +40,7 @@ function ActionMenu({ chatId, forkThread, isEditing, role }) {
<div className="mt-2 -ml-0.5 relative" ref={menuRef}>
<button
onClick={toggleMenu}
className="border-none text-[var(--theme-sidebar-footer-icon-fill)] hover:text-[var(--theme-sidebar-footer-icon-fill)] transition-colors duration-200"
className="border-none text-zinc-300 light:text-slate-500 transition-colors duration-200"
data-tooltip-id="action-menu"
data-tooltip-content={t("chat_window.more_actions")}
aria-label={t("chat_window.more_actions")}

View File

@ -1,4 +1,4 @@
import { Pencil } from "@phosphor-icons/react";
import { Info, Pencil } from "@phosphor-icons/react";
import { useState, useEffect, useRef } from "react";
import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next";
@ -53,14 +53,10 @@ export function EditMessageAction({ chatId = null, role, isEditing }) {
? t("chat_window.edit_prompt")
: t("chat_window.edit_response")
} `}
className="border-none text-zinc-300"
className="border-none text-zinc-300 light:text-slate-500"
aria-label={`Edit ${role === "user" ? t("chat_window.edit_prompt") : t("chat_window.edit_response")}`}
>
<Pencil
color="var(--theme-sidebar-footer-icon-fill)"
size={21}
className="mb-1"
/>
<Pencil size={21} className="mb-1" />
</button>
</div>
);
@ -75,17 +71,30 @@ export function EditMessageForm({
saveChanges,
}) {
const formRef = useRef(null);
const { t } = useTranslation();
function handleSaveMessage(e) {
function handleSubmit(e) {
e.preventDefault();
const form = new FormData(e.target);
const editedMessage = form.get("editedMessage");
const editedMessage = formRef.current.value;
saveChanges({ editedMessage, chatId, role, attachments });
window.dispatchEvent(
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
);
}
function handleSave() {
const editedMessage = formRef.current.value;
saveChanges({
editedMessage,
chatId,
role,
attachments,
saveOnly: true,
});
window.dispatchEvent(
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
);
}
function cancelEdits() {
window.dispatchEvent(
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
@ -94,36 +103,91 @@ export function EditMessageForm({
}
useEffect(() => {
if (!formRef || !formRef.current) return;
if (!formRef?.current) return;
formRef.current.focus();
adjustTextArea({ target: formRef.current });
}, [formRef]);
}, []);
if (role === "user") {
return (
<form
onSubmit={handleSubmit}
className="flex flex-col w-full max-w-[650px]"
>
<textarea
ref={formRef}
name="editedMessage"
spellCheck={Appearance.get("enableSpellCheck")}
className="text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden"
defaultValue={message}
onChange={adjustTextArea}
/>
<EditActionBar
onCancel={cancelEdits}
onSave={handleSave}
isUserMessage
/>
</form>
);
}
return (
<form onSubmit={handleSaveMessage} className="flex flex-col w-full">
<form
onSubmit={handleSubmit}
className="flex flex-col w-full max-w-[650px]"
>
<textarea
ref={formRef}
name="editedMessage"
spellCheck={Appearance.get("enableSpellCheck")}
className="text-white w-full rounded bg-theme-bg-secondary border border-white/20 active:outline-none focus:outline-none focus:ring-0 pr-16 pl-1.5 pt-1.5 resize-y"
className="text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden"
defaultValue={message}
onChange={adjustTextArea}
/>
<div className="mt-3 flex justify-center">
<button
type="submit"
className="border-none px-2 py-1 bg-gray-200 text-gray-700 font-medium rounded-md mr-2 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{t("chat_window.save_submit")}
</button>
<button
type="button"
className="border-none px-2 py-1 bg-historical-msg-system text-white font-medium rounded-md hover:bg-historical-msg-user/90 light:hover:text-white focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
onClick={cancelEdits}
>
{t("chat_window.cancel")}
</button>
</div>
<EditActionBar onCancel={cancelEdits} />
</form>
);
}
function EditActionBar({ onCancel, onSave, isUserMessage = false }) {
const { t } = useTranslation();
return (
<div className="mt-2 flex flex-col md:flex-row md:items-center justify-between gap-2 bg-zinc-800 light:bg-slate-200 rounded-lg p-2">
<div className="flex items-start gap-2">
<Info
size={12}
className="shrink-0 mt-0.5 text-zinc-200 light:text-slate-800"
/>
<span className="text-zinc-200 light:text-slate-800 text-xs leading-4">
{isUserMessage
? t("chat_window.edit_info_user")
: t("chat_window.edit_info_assistant")}
</span>
</div>
<div className="flex items-center gap-2 self-end shrink-0">
<button
type="button"
onClick={onCancel}
className="border-none text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300"
>
{t("chat_window.cancel")}
</button>
{isUserMessage && (
<button
type="button"
onClick={onSave}
className="border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300"
>
{t("chat_window.save")}
</button>
)}
<button
type="submit"
className="border-none bg-zinc-50 light:bg-slate-800 text-zinc-800 light:text-white text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-zinc-200 light:hover:bg-slate-800"
>
{isUserMessage ? t("chat_window.submit") : t("chat_window.save")}
</button>
</div>
</div>
);
}

View File

@ -1,6 +1,7 @@
import { formatDateTimeAsMoment } from "@/utils/directories";
import { numberWithCommas } from "@/utils/numbers";
import React, { useEffect, useState, useContext } from "react";
import { isMobile } from "react-device-detect";
const MetricsContext = React.createContext();
const SHOW_METRICS_KEY = "anythingllm_show_chat_metrics";
const SHOW_METRICS_EVENT = "anythingllm_show_metrics_change";
@ -116,7 +117,7 @@ export default function RenderMetrics({ metrics = {} }) {
// Inherit the showMetricsAutomatically state from the MetricsProvider so the state is shared across all chats
const { showMetricsAutomatically, setShowMetricsAutomatically } =
useContext(MetricsContext);
if (!metrics?.duration || !metrics?.outputTps) return null;
if (!metrics?.duration || !metrics?.outputTps || isMobile) return null;
return (
<button
@ -128,9 +129,9 @@ export default function RenderMetrics({ metrics = {} }) {
? "Click to only show metrics when hovering"
: "Click to show metrics as soon as they are available"
}
className={`border-none flex justify-end items-center gap-x-[8px] ${showMetricsAutomatically ? "opacity-100" : "opacity-0"} md:group-hover:opacity-100 transition-all duration-300`}
className={`border-none flex md:justify-end items-center gap-x-[8px] -ml-7 ${showMetricsAutomatically ? "opacity-100" : "opacity-0"} md:group-hover:opacity-100 transition-all duration-300`}
>
<p className="cursor-pointer text-xs font-mono text-theme-text-secondary opacity-50">
<p className="cursor-pointer text-xs font-mono text-zinc-400 light:text-slate-500">
{buildMetricsString(metrics)}
</p>
</button>

View File

@ -65,7 +65,7 @@ export default function AsyncTTSMessage({ slug, chatId }) {
? t("pause_tts_speech_message")
: t("chat_window.tts_speak_message")
}
className="border-none text-[var(--theme-sidebar-footer-icon-fill)]"
className="border-none text-zinc-300 light:text-slate-500"
aria-label={speaking ? "Pause speech" : "Speak message"}
>
{speaking ? (

View File

@ -3,6 +3,10 @@ import NativeTTSMessage from "./native";
import AsyncTTSMessage from "./asyncTts";
import PiperTTSMessage from "./piperTTS";
function WrapTTS({ children }) {
return <div className="mx-2">{children}</div>;
}
export default function TTSMessage({ slug, chatId, message }) {
const { settings, provider, loading } = useTTSProvider();
if (!chatId || loading) return null;
@ -11,16 +15,26 @@ export default function TTSMessage({ slug, chatId, message }) {
case "openai":
case "generic-openai":
case "elevenlabs":
return <AsyncTTSMessage chatId={chatId} slug={slug} />;
return (
<WrapTTS>
<AsyncTTSMessage chatId={chatId} slug={slug} />
</WrapTTS>
);
case "piper_local":
return (
<PiperTTSMessage
chatId={chatId}
voiceId={settings?.TTSPiperTTSVoiceModel}
message={message}
/>
<WrapTTS>
<PiperTTSMessage
chatId={chatId}
voiceId={settings?.TTSPiperTTSVoiceModel}
message={message}
/>
</WrapTTS>
);
default:
return <NativeTTSMessage chatId={chatId} message={message} />;
return (
<WrapTTS>
<NativeTTSMessage chatId={chatId} message={message} />
</WrapTTS>
);
}
}

View File

@ -41,7 +41,7 @@ export default function NativeTTSMessage({ chatId, message }) {
data-tooltip-content={
speaking ? "Pause TTS speech of message" : "TTS Speak message"
}
className="border-none text-[var(--theme-sidebar-footer-icon-fill)]"
className="border-none text-zinc-300 light:text-slate-500"
aria-label={speaking ? "Pause speech" : "Speak message"}
>
{speaking ? (

View File

@ -18,7 +18,6 @@ const Actions = ({
isEditing,
role,
metrics = {},
alignmentCls = "",
}) => {
const { t } = useTranslation();
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
@ -30,15 +29,21 @@ const Actions = ({
};
return (
<div className={`flex w-full justify-between items-center ${alignmentCls}`}>
<div
className={`flex w-full flex-wrap items-center gap-y-1 ${role === "user" ? "justify-end" : "justify-between"}`}
>
<div className="flex justify-start items-center gap-x-[8px]">
<CopyMessage message={message} />
<div className="md:group-hover:opacity-100 transition-all duration-300 md:opacity-0 flex justify-start items-center gap-x-[8px]">
<EditMessageAction
chatId={chatId}
role={role}
isEditing={isEditing}
/>
<div
className={`flex justify-start items-center gap-x-[8px] ${role === "user" ? "flex-row-reverse" : ""}`}
>
<CopyMessage message={message} />
<EditMessageAction
chatId={chatId}
role={role}
isEditing={isEditing}
/>
</div>
{isLastMessage && !isEditing && (
<RegenerateMessage
regenerateMessage={regenerateMessage}
@ -80,11 +85,10 @@ function FeedbackButton({
onClick={handleFeedback}
data-tooltip-id="feedback-button"
data-tooltip-content={tooltipContent}
className="text-zinc-300"
className="text-zinc-300 light:text-slate-500"
aria-label={tooltipContent}
>
<IconComponent
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
weight={isSelected ? "fill" : "regular"}
@ -105,21 +109,13 @@ function CopyMessage({ message }) {
onClick={() => copyText(message)}
data-tooltip-id="copy-assistant-text"
data-tooltip-content={t("chat_window.copy")}
className="text-zinc-300"
className="text-zinc-300 light:text-slate-500"
aria-label={t("chat_window.copy")}
>
{copied ? (
<Check
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
/>
<Check size={20} className="mb-1" />
) : (
<Copy
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
/>
<Copy size={20} className="mb-1" />
)}
</button>
</div>
@ -136,15 +132,10 @@ function RegenerateMessage({ regenerateMessage, chatId }) {
onClick={() => regenerateMessage(chatId)}
data-tooltip-id="regenerate-assistant-text"
data-tooltip-content={t("chat_window.regenerate_response")}
className="border-none text-zinc-300"
className="border-none text-zinc-300 light:text-slate-500"
aria-label={t("chat_window.regenerate")}
>
<ArrowsClockwise
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
weight="fill"
/>
<ArrowsClockwise size={20} className="mb-1" weight="fill" />
</button>
</div>
);

View File

@ -1,9 +1,7 @@
import React, { memo } from "react";
import React, { memo, useEffect, useRef, useState } from "react";
import { Info, Warning } from "@phosphor-icons/react";
import UserIcon from "../../../../UserIcon";
import Actions from "./Actions";
import renderMarkdown from "@/utils/chat/markdown";
import { userFromStorage } from "@/utils/request";
import Citations from "../Citation";
import { v4 } from "uuid";
import DOMPurify from "@/utils/chat/purify";
@ -36,7 +34,6 @@ const HistoricalMessage = ({
saveEditedMessage,
forkThread,
metrics = {},
alignmentCls = "",
}) => {
const { t } = useTranslation();
const { isEditing } = useEditMessage({ chatId, role });
@ -53,91 +50,120 @@ const HistoricalMessage = ({
const isRefusalMessage =
role === "assistant" && message === chatQueryRefusalResponse(workspace);
if (completeDelete) return null;
if (!!error) {
return (
<div
key={uuid}
className={`flex justify-center items-end w-full bg-theme-bg-chat`}
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className={`flex gap-x-5 ${alignmentCls}`}>
<ProfileImage role={role} workspace={workspace} />
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className="inline-block">
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
</span>
<p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
{error}
</p>
</div>
<div key={uuid} className="flex justify-start w-full">
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className="inline-block">
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
</span>
<p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
{error}
</p>
</div>
</div>
</div>
);
}
if (completeDelete) return null;
if (role === "user") {
if (isEditing) {
return (
<div key={uuid} className="flex justify-end w-full py-4 px-4">
<EditMessageForm
role={role}
chatId={chatId}
message={message}
attachments={attachments}
adjustTextArea={adjustTextArea}
saveChanges={saveEditedMessage}
/>
</div>
);
}
return (
<div
key={uuid}
onAnimationEnd={onEndAnimation}
className={`${
isDeleted ? "animate-remove" : ""
} flex justify-center items-end w-full group bg-theme-bg-chat`}
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className={`flex gap-x-5 ${alignmentCls}`}>
<div className="flex flex-col items-center">
<ProfileImage role={role} workspace={workspace} />
<div className="mt-1 -mb-10">
{role === "assistant" && (
<TTSMessage
slug={workspace?.slug}
chatId={chatId}
message={message}
/>
)}
</div>
</div>
{isEditing ? (
<EditMessageForm
role={role}
chatId={chatId}
message={message}
attachments={attachments}
adjustTextArea={adjustTextArea}
saveChanges={saveEditedMessage}
/>
) : (
<div className="break-words">
return (
<div
key={uuid}
onAnimationEnd={onEndAnimation}
className={`${isDeleted ? "animate-remove" : ""} flex justify-end w-full group`}
>
<div className="py-4 px-4 flex flex-col items-end">
<div className="bg-zinc-800 light:bg-slate-100 rounded-[20px] rounded-br-none px-4 py-3.5 max-w-[600px] [&_p]:m-0">
<TruncatableContent>
<RenderChatContent
role={role}
message={message}
messageId={uuid}
/>
{isRefusalMessage && (
<Link
data-tooltip-id="query-refusal-info"
data-tooltip-content={`${t("chat.refusal.tooltip-description")}`}
className="!no-underline group !flex w-fit"
to={paths.chatModes()}
target="_blank"
>
<div className="flex flex-row items-center gap-x-1 group-hover:opacity-100 opacity-60 w-fit">
<Info className="text-theme-text-secondary" />
<p className="!m-0 !p-0 text-theme-text-secondary !no-underline text-xs cursor-pointer">
{t("chat.refusal.tooltip-title")}
</p>
</div>
</Link>
)}
<ChatAttachments attachments={attachments} />
</div>
)}
</div>
<div className="flex gap-x-5 ml-14">
</TruncatableContent>
</div>
<Actions
message={message}
feedbackScore={feedbackScore}
chatId={chatId}
slug={workspace?.slug}
isLastMessage={isLastMessage}
regenerateMessage={regenerateMessage}
isEditing={isEditing}
role={role}
forkThread={forkThread}
metrics={metrics}
/>
</div>
</div>
);
}
return (
<div
key={uuid}
onAnimationEnd={onEndAnimation}
className={`${isDeleted ? "animate-remove" : ""} flex justify-start w-full group`}
>
<div className="py-4 px-4 md:pl-0 flex flex-col w-full">
{isEditing ? (
<EditMessageForm
role={role}
chatId={chatId}
message={message}
attachments={attachments}
adjustTextArea={adjustTextArea}
saveChanges={saveEditedMessage}
/>
) : (
<div className="break-words">
<RenderChatContent role={role} message={message} messageId={uuid} />
{isRefusalMessage && (
<Link
data-tooltip-id="query-refusal-info"
data-tooltip-content={`${t("chat.refusal.tooltip-description")}`}
className="!no-underline group !flex w-fit"
to={paths.chatModes()}
target="_blank"
>
<div className="flex flex-row items-center gap-x-1 group-hover:opacity-100 opacity-60 w-fit">
<Info className="text-theme-text-secondary" />
<p className="!m-0 !p-0 text-theme-text-secondary !no-underline text-xs cursor-pointer">
{t("chat.refusal.tooltip-title")}
</p>
</div>
</Link>
)}
<ChatAttachments attachments={attachments} />
</div>
)}
<div className="flex items-start md:items-center gap-x-1">
<TTSMessage
slug={workspace?.slug}
chatId={chatId}
message={message}
/>
<Actions
message={message}
feedbackScore={feedbackScore}
@ -149,7 +175,6 @@ const HistoricalMessage = ({
role={role}
forkThread={forkThread}
metrics={metrics}
alignmentCls={alignmentCls}
/>
</div>
{role === "assistant" && <Citations sources={sources} />}
@ -158,29 +183,6 @@ const HistoricalMessage = ({
);
};
function ProfileImage({ role, workspace }) {
if (role === "assistant" && workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return (
<UserIcon
user={{
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
}}
role={role}
/>
);
}
export default memo(
HistoricalMessage,
// Skip re-render the historical message:
@ -199,18 +201,73 @@ export default memo(
function ChatAttachments({ attachments = [] }) {
if (!attachments.length) return null;
return (
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-4 mt-4">
{attachments.map((item) => (
<img
alt={`Attachment: ${item.name}`}
key={item.name}
src={item.contentString}
className="max-w-[300px] rounded-md"
className="w-[120px] h-[120px] object-cover rounded-lg"
/>
))}
</div>
);
}
function TruncatableContent({ children }) {
const contentRef = useRef(null);
const [isExpanded, setIsExpanded] = useState(false);
const [isOverflowing, setIsOverflowing] = useState(false);
const { t } = useTranslation();
useEffect(() => {
if (contentRef.current) {
setIsOverflowing(contentRef.current.scrollHeight > 250);
}
}, []);
const showTruncation = !isExpanded && isOverflowing;
return (
<>
<div className="relative">
<div
ref={contentRef}
className={showTruncation ? "max-h-[250px] overflow-hidden" : ""}
>
{children}
</div>
{showTruncation && (
<>
<div
className="absolute bottom-0 left-0 right-0 h-[36px] light:hidden pointer-events-none"
style={{
background:
"linear-gradient(180deg, rgba(39, 39, 42, 0.00) 0%, rgba(39, 39, 42, 0.65) 50%, #27272A 100%)",
}}
/>
<div
className="absolute bottom-0 left-0 right-0 h-[36px] hidden light:block pointer-events-none"
style={{
background:
"linear-gradient(180deg, rgba(241, 245, 249, 0.00) 0%, rgba(241, 245, 249, 0.65) 50%, #F1F5F9 100%)",
}}
/>
</>
)}
</div>
{isOverflowing && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-zinc-300 light:text-slate-700 hover:text-white light:hover:text-slate-900 text-xs font-medium leading-4 mt-2"
>
{isExpanded ? t("chat_window.see_less") : t("chat_window.see_more")}
</button>
)}
</>
);
}
const RenderChatContent = memo(
({ role, message, messageId }) => {
// If the message is not from the assistant, we can render it directly
@ -218,7 +275,7 @@ const RenderChatContent = memo(
if (role !== "assistant")
return (
<span
className="flex flex-col gap-y-1"
className="flex flex-col gap-y-1 text-white light:text-slate-900"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)),
}}
@ -252,7 +309,7 @@ const RenderChatContent = memo(
<ThoughtChainComponent content={thoughtChain} messageId={messageId} />
)}
<span
className="flex flex-col gap-y-1"
className="flex flex-col gap-y-1 text-white light:text-slate-900"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(msgToRender)),
}}

View File

@ -1,7 +1,6 @@
/* eslint-disable react-hooks/refs */
import { memo, useRef, useEffect } from "react";
import { Warning } from "@phosphor-icons/react";
import UserIcon from "../../../../UserIcon";
import renderMarkdown from "@/utils/chat/markdown";
import Citations from "../Citation";
import {
@ -11,28 +10,14 @@ import {
ThoughtChainComponent,
} from "../ThoughtContainer";
const PromptReply = ({
uuid,
reply,
pending,
error,
workspace,
sources = [],
}) => {
const assistantBackgroundColor = "bg-theme-bg-chat";
const PromptReply = ({ uuid, reply, pending, error, sources = [] }) => {
if (!reply && sources.length === 0 && !pending && !error) return null;
if (pending) {
return (
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<div className="mt-3 ml-5 dot-falling light:invert"></div>
</div>
<div className="flex justify-start w-full">
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
<div className="mt-3 ml-1 dot-falling light:invert"></div>
</div>
</div>
);
@ -40,61 +25,32 @@ const PromptReply = ({
if (error) {
return (
<div
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
>
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
<span className="text-xs">Reason: {error || "unknown"}</span>
</span>
</div>
<div className="flex justify-start w-full">
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
<span className="inline-block p-2 rounded-lg bg-red-50 text-red-500">
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not respond
to message.
<span className="text-xs">Reason: {error || "unknown"}</span>
</span>
</div>
</div>
);
}
return (
<div
key={uuid}
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
>
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
<div className="flex gap-x-5">
<WorkspaceProfileImage workspace={workspace} />
<RenderAssistantChatContent
key={`${uuid}-prompt-reply-content`}
message={reply}
messageId={uuid}
/>
</div>
<div key={uuid} className="flex justify-start w-full">
<div className="py-4 pl-0 pr-4 flex flex-col w-full">
<RenderAssistantChatContent
key={`${uuid}-prompt-reply-content`}
message={reply}
messageId={uuid}
/>
<Citations sources={sources} />
</div>
</div>
);
};
export function WorkspaceProfileImage({ workspace }) {
if (!!workspace.pfpUrl) {
return (
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
<img
src={workspace.pfpUrl}
alt="Workspace profile picture"
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
/>
</div>
);
}
return <UserIcon user={{ uid: workspace.slug }} role="assistant" />;
}
function RenderAssistantChatContent({ message, messageId }) {
const contentRef = useRef("");
const thoughtChainRef = useRef(null);

View File

@ -15,22 +15,25 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
}
return (
<div className="flex justify-center w-full">
<div className="w-full max-w-[80%] flex flex-col">
<div className=" w-full max-w-[800px]">
<div className="flex justify-center w-full pr-4">
<div className="w-full flex flex-col">
<div className="w-full">
<div
onClick={handleExpandClick}
style={{ borderRadius: "6px" }}
className={`${!previousThoughts?.length ? "" : `${previousThoughts?.length ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
style={{
transition: "all 0.1s ease-in-out",
borderRadius: "16px",
}}
className="relative bg-zinc-800 light:bg-slate-100 p-4"
>
<div className="w-7 h-7 flex justify-center flex-shrink-0 items-center">
<div className="absolute top-4 left-4 w-[18px] h-[18px]">
{isThinking ? (
<video
autoPlay
loop
muted
playsInline
className="w-8 h-8 scale-150 transition-opacity duration-200 light:invert light:opacity-50"
className="w-[18px] h-[18px] scale-[165%] transition-opacity duration-200 light:invert light:opacity-50"
data-tooltip-id="agent-thinking"
data-tooltip-content="Agent is thinking..."
aria-label="Agent is thinking..."
@ -41,57 +44,53 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
<img
src={AgentStatic}
alt="Agent complete"
className="w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50"
className="w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50"
data-tooltip-id="agent-thinking"
data-tooltip-content="Agent has finished thinking"
aria-label="Agent has finished thinking"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${isExpanded ? "" : "max-h-6"}`}
{previousThoughts?.length > 0 && (
<button
onClick={handleExpandClick}
className="absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors"
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<div className="text-theme-text-secondary font-mono leading-6">
{!isExpanded ? (
<span className="block w-full truncate mt-[2px]">
{currentThought.content}
</span>
) : (
<>
{previousThoughts.map((thought, index) => (
<div
key={`cot-${thought.uuid || index}`}
className="mb-2"
>
{thought.content}
</div>
))}
<div>{currentThought.content}</div>
</>
)}
</div>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
)}
<div
className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? "" : "overflow-hidden max-h-[18px]"}`}
>
<div className="text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px]">
{!isExpanded ? (
<span className="block w-full truncate">
{currentThought.content}
</span>
) : (
<>
{previousThoughts.map((thought, index) => (
<div
key={`cot-${thought.uuid || index}`}
className="mb-2"
>
{thought.content}
</div>
))}
<div>{currentThought.content}</div>
</>
)}
</div>
</div>
<div className="flex items-center gap-x-2">
{previousThoughts?.length > 0 && (
<button
onClick={handleExpandClick}
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
)}
</div>
</div>
</div>
</div>

View File

@ -141,50 +141,63 @@ export const ThoughtChainComponent = forwardRef(
}
return (
<div className="flex justify-start items-end transition-all duration-200 w-full md:max-w-[800px]">
<div className="pb-2 w-full flex gap-x-5 flex-col relative">
<div
style={{
transition: "all 0.1s ease-in-out",
borderRadius: "6px",
}}
className={`${isExpanded ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
>
<div className="flex justify-center w-full">
<div className="w-full flex flex-col">
<div className="w-full">
<div
className={`w-7 h-7 flex justify-center flex-shrink-0 ${!isExpanded ? "items-center" : "items-start pt-[2px]"}`}
style={{
transition: "all 0.1s ease-in-out",
borderRadius: "16px",
}}
className="relative bg-zinc-800 light:bg-slate-100 p-4"
>
{isThinking || isComplete ? (
<>
<video
autoPlay
loop
muted
playsInline
className={`w-7 h-7 transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model is thinking..."
aria-label="Model is thinking..."
>
<source src={ThinkingAnimation} type="video/webm" />
</video>
<img
src={ThinkingStatic}
alt="Thinking complete"
className={`w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model has finished thinking"
aria-label="Model has finished thinking"
/>
</>
) : null}
</div>
<div className="flex-1 min-w-0">
<div
className={`overflow-hidden transition-all transform duration-300 ease-in-out origin-top ${isExpanded ? "" : "max-h-6"}`}
>
<div
className={`text-theme-text-secondary font-mono leading-6 ${isExpanded ? "-ml-[5.5px] -mt-[4px]" : "mt-[2px]"}`}
<div className="absolute top-4 left-4 w-[18px] h-[18px]">
{isThinking || isComplete ? (
<>
<video
autoPlay
loop
muted
playsInline
className={`w-[18px] h-[18px] scale-[115%] transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model is thinking..."
aria-label="Model is thinking..."
>
<source src={ThinkingAnimation} type="video/webm" />
</video>
<img
src={ThinkingStatic}
alt="Thinking complete"
className={`w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model has finished thinking"
aria-label="Model has finished thinking"
/>
</>
) : null}
</div>
{canExpand && (
<button
onClick={handleExpandClick}
className="absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors"
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
)}
<div
className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? "" : "overflow-hidden max-h-[18px]"}`}
>
<div className="text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px] [&_p]:m-0">
<span
className={`block w-full ${!isExpanded ? "truncate" : ""}`}
dangerouslySetInnerHTML={{
@ -198,25 +211,6 @@ export const ThoughtChainComponent = forwardRef(
</div>
</div>
</div>
<div className="flex items-center gap-x-2">
{canExpand ? (
<button
onClick={handleExpandClick}
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
) : null}
</div>
</div>
</div>
</div>

View File

@ -20,7 +20,6 @@ import paths from "@/utils/paths";
import Appearance from "@/models/appearance";
import useTextSize from "@/hooks/useTextSize";
import useChatHistoryScrollHandle from "@/hooks/useChatHistoryScrollHandle";
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
import { ThoughtExpansionProvider } from "./ThoughtContainer";
export default forwardRef(function (
@ -42,7 +41,6 @@ export default forwardRef(function (
const isStreaming = history[history.length - 1]?.animate;
const { showScrollbar } = Appearance.getSettings();
const { textSizeClass } = useTextSize();
const { getMessageAlignment } = useChatMessageAlignment();
useEffect(() => {
if (!isUserScrolling && (isAtBottom || isStreaming)) {
@ -52,7 +50,7 @@ export default forwardRef(function (
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isBottom = scrollHeight - scrollTop === clientHeight;
const isBottom = scrollHeight - scrollTop - clientHeight < 2;
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
@ -98,10 +96,28 @@ export default forwardRef(function (
chatId,
role,
attachments = [],
saveOnly = false,
}) => {
if (!editedMessage) return; // Don't save empty edits.
// if the edit was a user message, we will auto-regenerate the response and delete all
// "Save" on a user message: update the prompt text without regenerating
if (role === "user" && saveOnly) {
const updatedHistory = [...history];
const targetIdx = history.findIndex((msg) => msg.chatId === chatId);
if (targetIdx < 0) return;
updatedHistory[targetIdx].content = editedMessage;
updateHistory(updatedHistory);
await Workspace.updateChat(
workspace.slug,
threadSlug,
chatId,
editedMessage,
"user"
);
return;
}
// "Submit" on a user message: auto-regenerate the response and delete all
// messages post modified message
if (role === "user") {
// remove all messages after the edited message
@ -133,7 +149,7 @@ export default forwardRef(function (
if (targetIdx < 0) return;
updatedHistory[targetIdx].content = editedMessage;
updateHistory(updatedHistory);
await Workspace.updateChatResponse(
await Workspace.updateChat(
workspace.slug,
threadSlug,
chatId,
@ -163,7 +179,6 @@ export default forwardRef(function (
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
getMessageAlignment,
}),
[
workspace,
@ -191,36 +206,38 @@ export default forwardRef(function (
return (
<ThoughtExpansionProvider>
<div
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col justify-start ${showScrollbar ? "show-scrollbar" : "no-scroll"}`}
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col items-center justify-start ${showScrollbar ? "show-scrollbar" : "no-scroll"}`}
id="chat-history"
ref={chatHistoryRef}
onScroll={handleScroll}
>
{compiledHistory.map((item, index) =>
Array.isArray(item) ? renderStatusResponse(item, index) : item
)}
<div className="w-full max-w-[750px]">
{compiledHistory.map((item, index) =>
Array.isArray(item) ? renderStatusResponse(item, index) : item
)}
</div>
{showing && (
<ManageWorkspace
hideModal={hideModal}
providedSlug={workspace.slug}
/>
)}
{!isAtBottom && (
<div className="fixed bottom-40 right-10 md:right-20 z-50 cursor-pointer animate-pulse">
<div className="flex flex-col items-center">
<div
className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"
onClick={() => {
scrollToBottom(isStreaming ? false : true);
setIsUserScrolling(false);
}}
>
<ArrowDown weight="bold" className="text-white/60 w-5 h-5" />
</div>
</div>
{!isAtBottom && (
<div className="absolute bottom-40 right-10 z-50 cursor-pointer animate-pulse">
<div className="flex flex-col items-center">
<div
className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"
onClick={() => {
scrollToBottom(isStreaming ? false : true);
setIsUserScrolling(false);
}}
>
<ArrowDown weight="bold" className="text-white/60 w-5 h-5" />
</div>
</div>
)}
</div>
</div>
)}
</ThoughtExpansionProvider>
);
});
@ -245,7 +262,6 @@ const getLastMessageInfo = (history) => {
* @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.
* @param {Function} param0.saveEditedMessage - The function to save the edited message.
* @param {Function} param0.forkThread - The function to fork the thread.
* @param {Function} param0.getMessageAlignment - The function to get the alignment of the message (returns class).
* @returns {Array} The compiled history of messages.
*/
function buildMessages({
@ -254,7 +270,6 @@ function buildMessages({
regenerateAssistantMessage,
saveEditedMessage,
forkThread,
getMessageAlignment,
}) {
return history.reduce((acc, props, index) => {
const isLastBotReply =
@ -270,9 +285,7 @@ function buildMessages({
}
if (props.type === "rechartVisualize" && !!props.content) {
acc.push(
<Chartable key={props.uuid} workspace={workspace} props={props} />
);
acc.push(<Chartable key={props.uuid} props={props} />);
} else if (isLastBotReply && props.animate) {
acc.push(
<PromptReply
@ -282,7 +295,6 @@ function buildMessages({
pending={props.pending}
sources={props.sources}
error={props.error}
workspace={workspace}
closed={props.closed}
/>
);
@ -304,7 +316,6 @@ function buildMessages({
saveEditedMessage={saveEditedMessage}
forkThread={forkThread}
metrics={props.metrics}
alignmentCls={getMessageAlignment?.(props.role)}
/>
);
}

View File

@ -1,5 +1,6 @@
import { Tooltip } from "react-tooltip";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
/**
* Set the tooltips for the chat container in bulk.
@ -16,6 +17,8 @@ import { createPortal } from "react-dom";
* @returns
*/
export function ChatTooltips() {
const { t } = useTranslation();
return (
<>
<Tooltip
@ -96,6 +99,13 @@ export function ChatTooltips() {
delayShow={300}
className="tooltip !text-xs"
/>
<Tooltip
id="agent-skill-disabled-tooltip"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
content={t("chat_window.agent_skills_disabled_in_session")}
/>
<DocumentLevelTooltip />
</>
);

View File

@ -1,144 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { Tooltip } from "react-tooltip";
import { At } from "@phosphor-icons/react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
export default function AvailableAgentsButton({ showing, setShowAgents }) {
const { t } = useTranslation();
const agentSessionActive = useIsAgentSessionActive();
if (agentSessionActive) return null;
return (
<div
id="agent-list-btn"
data-tooltip-id="tooltip-agent-list-btn"
data-tooltip-content={t("chat_window.agents")}
aria-label={t("chat_window.agents")}
onClick={() => setShowAgents(!showing)}
className={`flex justify-center items-center cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ${
showing ? "!opacity-100" : ""
}`}
>
<At
color="var(--theme-sidebar-footer-icon-fill)"
className="w-[20px] h-[20px] pointer-events-none text-theme-text-primary"
/>
<Tooltip
id="tooltip-agent-list-btn"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}
function AbilityTag({ text }) {
return (
<div className="px-2 bg-theme-action-menu-item-hover text-theme-text-secondary text-xs w-fit rounded-sm">
<p>{text}</p>
</div>
);
}
export function AvailableAgents({
showing,
setShowing,
sendCommand,
promptRef,
centered = false,
}) {
const formRef = useRef(null);
const agentSessionActive = useIsAgentSessionActive();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
/*
* @checklist-item
* If the URL has the agent param, open the agent menu for the user
* automatically when the component mounts.
*/
useEffect(() => {
if (searchParams.get("action") === "set-agent-chat" && !showing)
handleAgentClick();
}, [promptRef.current]);
useEffect(() => {
function listenForOutsideClick() {
if (!showing || !formRef.current) return false;
document.addEventListener("click", closeIfOutside);
}
listenForOutsideClick();
}, [showing, formRef.current]);
const closeIfOutside = ({ target }) => {
if (target.id === "agent-list-btn") return;
const isOutside = !formRef?.current?.contains(target);
if (!isOutside) return;
setShowing(false);
};
const handleAgentClick = () => {
setShowing(false);
sendCommand({ text: "@agent " });
promptRef?.current?.focus();
};
if (agentSessionActive) return null;
return (
<>
<div hidden={!showing}>
<div
className={
centered
? "w-full flex justify-center md:justify-start absolute top-full mt-2 left-0 z-10 px-4 md:px-0 md:pl-[57px]"
: "flex justify-center md:justify-start absolute bottom-[130px] md:bottom-[150px] left-0 right-0 z-10 max-w-[750px] mx-auto px-4 md:px-0 md:pl-[57px]"
}
>
<div
ref={formRef}
className="w-[600px] p-2 bg-theme-action-menu-bg rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex overflow-y-auto max-h-[200px] no-scroll"
>
<button
onClick={handleAgentClick}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start group"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-theme-text-primary text-sm">
<b>{t("chat_window.at_agent")}</b>
{t("chat_window.default_agent_description")}
</div>
<div className="flex flex-wrap gap-2 mt-2">
<AbilityTag text="rag-search" />
<AbilityTag text="web-scraping" />
<AbilityTag text="web-browsing" />
<AbilityTag text="save-file-to-browser" />
<AbilityTag text="list-documents" />
<AbilityTag text="summarize-document" />
<AbilityTag text="chart-generation" />
</div>
</div>
</button>
<button
type="button"
disabled={true}
className="w-full rounded-xl flex flex-col justify-start group"
>
<div className="w-full flex-col text-center flex pointer-events-none">
<div className="text-theme-text-secondary text-xs italic">
{t("chat_window.custom_agents_coming_soon")}
</div>
</div>
</button>
</div>
</div>
</div>
</>
);
}
export function useAvailableAgents() {
const [showAgents, setShowAgents] = useState(false);
return { showAgents, setShowAgents };
}

View File

@ -1,4 +1,4 @@
import { PaperclipHorizontal } from "@phosphor-icons/react";
import { Plus } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
import { useRef, useState, useEffect } from "react";
@ -98,15 +98,16 @@ export default function AttachItem({
type="button"
onClick={handleClick}
onPointerEnter={fetchFiles}
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer`}
className="group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200"
>
<div className="relative">
<PaperclipHorizontal
color="var(--theme-sidebar-footer-icon-fill)"
className="w-[20px] h-[20px] pointer-events-none text-white rotate-90 -scale-y-100"
<Plus
size={18}
className="pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0"
weight="bold"
/>
{files.length > 0 && (
<div className="absolute -top-2 right-[1%] bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center">
<div className="absolute -top-2.5 -right-2 bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center">
{files.length}
</div>
)}

View File

@ -1,7 +1,6 @@
import useGetProviderModels, {
DISABLED_PROVIDERS,
} from "@/hooks/useGetProvidersModels";
import { useTranslation } from "react-i18next";
export default function ChatModelSelection({
provider,
@ -11,110 +10,81 @@ export default function ChatModelSelection({
}) {
const { defaultModels, customModels, loading } =
useGetProviderModels(provider);
const { t } = useTranslation();
if (DISABLED_PROVIDERS.includes(provider)) return null;
if (loading) {
return (
<div>
<div className="flex flex-col">
<label htmlFor="name" className="block input-label">
{t("chat_window.workspace_llm_manager.available_models", {
provider,
})}
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t(
"chat_window.workspace_llm_manager.available_models_description"
)}
</p>
</div>
<select
required={true}
disabled={true}
className="border-theme-modal-border border border-solid bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
>
<option disabled={true} selected={true}>
-- waiting for models --
</option>
</select>
</div>
<select
required={true}
disabled={true}
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-not-allowed"
>
<option disabled={true} selected={true}>
-- waiting for models --
</option>
</select>
);
}
return (
<div>
<div className="flex flex-col">
<label htmlFor="name" className="block input-label">
{t("chat_window.workspace_llm_manager.available_models", {
provider,
<select
id="workspace-llm-model-select"
required={true}
value={selectedLLMModel}
onChange={(e) => {
setHasChanges(true);
setSelectedLLMModel(e.target.value);
}}
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-pointer"
>
{defaultModels.length > 0 && (
<optgroup label="General models">
{defaultModels.map((model) => {
return (
<option
key={model}
value={model}
selected={selectedLLMModel === model}
>
{model}
</option>
);
})}
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t("chat_window.workspace_llm_manager.available_models_description")}
</p>
</div>
<select
id="workspace-llm-model-select"
required={true}
value={selectedLLMModel}
onChange={(e) => {
setHasChanges(true);
setSelectedLLMModel(e.target.value);
}}
className="border-theme-modal-border border border-solid bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
>
{defaultModels.length > 0 && (
<optgroup label="General models">
{defaultModels.map((model) => {
return (
<option
key={model}
value={model}
selected={selectedLLMModel === model}
>
{model}
</option>
);
})}
</optgroup>
)}
{Array.isArray(customModels) && customModels.length > 0 && (
<optgroup label="Discovered models">
{customModels.map((model) => {
return (
</optgroup>
)}
{Array.isArray(customModels) && customModels.length > 0 && (
<optgroup label="Discovered models">
{customModels.map((model) => {
return (
<option
key={model.id}
value={model.id}
selected={selectedLLMModel === model.id}
>
{model.id}
</option>
);
})}
</optgroup>
)}
{/* For providers like TogetherAi where we partition model by creator entity. */}
{!Array.isArray(customModels) && Object.keys(customModels).length > 0 && (
<>
{Object.entries(customModels).map(([organization, models]) => (
<optgroup key={organization} label={organization}>
{models.map((model) => (
<option
key={model.id}
value={model.id}
selected={selectedLLMModel === model.id}
>
{model.id}
{model.name}
</option>
);
})}
</optgroup>
)}
{/* For providers like TogetherAi where we partition model by creator entity. */}
{!Array.isArray(customModels) &&
Object.keys(customModels).length > 0 && (
<>
{Object.entries(customModels).map(([organization, models]) => (
<optgroup key={organization} label={organization}>
{models.map((model) => (
<option
key={model.id}
value={model.id}
selected={selectedLLMModel === model.id}
>
{model.name}
</option>
))}
</optgroup>
))}
</>
)}
</select>
</div>
</optgroup>
))}
</>
)}
</select>
);
}

View File

@ -1,3 +1,4 @@
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
export default function LLMSelectorSidePanel({
@ -9,31 +10,42 @@ export default function LLMSelectorSidePanel({
const { t } = useTranslation();
return (
<div className="w-[40%] h-full flex flex-col gap-y-1 border-r-2 border-theme-modal-border py-2 px-[5px]">
<input
id="llm-search-input"
type="search"
placeholder={t("chat_window.workspace_llm_manager.search")}
onChange={onSearchChange}
className="search-input bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder outline-none text-sm rounded-lg px-2 py-2 w-full h-[32px] border-theme-modal-border border border-solid"
/>
<div className="flex flex-col gap-y-2 overflow-y-scroll ">
<div className="w-[40%] h-full flex flex-col gap-4 p-2 border-r border-zinc-700 light:border-slate-300">
<div className="relative shrink-0 mx-2">
<MagnifyingGlass
size={14}
className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400 light:text-slate-400"
weight="bold"
/>
<input
id="llm-search-input"
type="search"
placeholder={t("chat_window.workspace_llm_manager.search")}
onChange={onSearchChange}
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 placeholder:text-zinc-500 light:placeholder:text-slate-400 text-sm rounded-lg pl-8 pr-2.5 h-8 w-full outline-none border border-zinc-900 light:border-slate-400"
/>
</div>
<div className="flex flex-col gap-0 overflow-y-auto min-h-0 flex-1">
{availableProviders.map((llm) => (
<button
key={llm.value}
type="button"
data-llm-value={llm.value}
className={`border-none hover:cursor-pointer hover:bg-theme-checklist-item-bg-hover flex gap-x-2 items-center p-2 rounded-md ${selectedLLMProvider === llm.value ? "bg-theme-checklist-item-bg" : ""}`}
className={`border-none cursor-pointer flex gap-2 items-center px-2.5 py-1.5 rounded-md transition-colors ${
selectedLLMProvider === llm.value
? "bg-zinc-700 light:bg-slate-200"
: "hover:bg-zinc-700/50 light:hover:bg-slate-100 bg-transparent"
}`}
onClick={() => onProviderClick(llm.value)}
>
<img
src={llm.logo}
alt={`${llm.name} logo`}
className="w-6 h-6 rounded-md"
className="w-6 h-6 rounded"
/>
<div className="flex flex-col">
<div className="text-xs text-theme-text-primary">{llm.name}</div>
</div>
<span className="text-sm text-white light:text-slate-900">
{llm.name}
</span>
</button>
))}
</div>

View File

@ -1,6 +1,6 @@
import { createPortal } from "react-dom";
import ModalWrapper from "@/components/ModalWrapper";
import { X } from "@phosphor-icons/react";
import { X, WarningCircle } from "@phosphor-icons/react";
import System from "@/models/system";
import showToast from "@/utils/toast";
import { useTranslation } from "react-i18next";
@ -93,17 +93,23 @@ export function NoSetupWarning({ showing, onSetupClick }) {
if (!showing) return null;
return (
<button
type="button"
onClick={onSetupClick}
className="border border-blue-500 rounded-lg p-2 flex flex-col items-center gap-y-2 bg-blue-600/10 text-blue-600 hover:bg-blue-600/20 transition-all duration-300"
>
<p className="text-sm text-center">
<b>{t("chat_window.workspace_llm_manager.missing_credentials")}</b>
<div className="flex items-start gap-1.5">
<WarningCircle
size={16}
className="text-white light:text-slate-800 shrink-0 mt-0.5"
/>
<p className="text-[13px] text-white light:text-slate-800 leading-5">
{t("chat_window.workspace_llm_manager.missing_credentials")}{" "}
<span
onClick={onSetupClick}
className="text-sky-400 font-semibold cursor-pointer hover:underline"
role="button"
>
{t(
"chat_window.workspace_llm_manager.missing_credentials_description"
)}
</span>
</p>
<p className="text-xs text-center">
{t("chat_window.workspace_llm_manager.missing_credentials_description")}
</p>
</button>
</div>
);
}

View File

@ -16,7 +16,10 @@ import showToast from "@/utils/toast";
import Workspace from "@/models/workspace";
import System from "@/models/system";
export default function LLMSelectorModal({ workspaceSlug = null }) {
export default function LLMSelectorModal({
workspaceSlug = null,
initialProvider = null,
}) {
const { slug: urlSlug } = useParams();
const slug = urlSlug ?? workspaceSlug;
const { t } = useTranslation();
@ -36,14 +39,22 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
setLoading(true);
Promise.all([Workspace.bySlug(slug), System.keys()])
.then(([workspace, systemSettings]) => {
const selectedLLMProvider =
const savedProvider =
workspace.chatProvider ?? systemSettings.LLMProvider;
const selectedLLMModel = workspace.chatModel ?? systemSettings.LLMModel;
const savedModel = workspace.chatModel ?? systemSettings.LLMModel;
const providerToSelect = initialProvider ?? savedProvider;
setSettings(systemSettings);
setSelectedLLMProvider(selectedLLMProvider);
autoScrollToSelectedLLMProvider(selectedLLMProvider);
setSelectedLLMModel(selectedLLMModel);
setSelectedLLMProvider(providerToSelect);
autoScrollToSelectedLLMProvider(providerToSelect);
setSelectedLLMModel(savedModel);
if (initialProvider && initialProvider !== savedProvider) {
setHasChanges(true);
setMissingCredentials(
hasMissingCredentials(systemSettings, initialProvider)
);
}
})
.finally(() => setLoading(false));
}, [slug]);
@ -87,14 +98,18 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
}
}
const providerName =
WORKSPACE_LLM_PROVIDERS.find((p) => p.value === selectedLLMProvider)
?.name || selectedLLMProvider;
if (loading) {
return (
<div
id="llm-selector-modal"
className="w-full h-[500px] p-0 overflow-y-scroll flex flex-col items-center justify-center"
className="w-full h-[388px] flex flex-col items-center justify-center gap-2"
>
<PreLoader size={12} />
<p className="text-theme-text-secondary text-sm mt-2">
<p className="text-zinc-400 light:text-slate-500 text-sm">
{t("chat_window.workspace_llm_manager.loading_workspace_settings")}
</p>
</div>
@ -102,17 +117,36 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
}
return (
<div
id="llm-selector-modal"
className="w-full h-[500px] p-0 overflow-y-scroll flex"
>
<div id="llm-selector-modal" className="w-full h-[388px] flex">
<LLMSelectorSidePanel
availableProviders={availableProviders}
selectedLLMProvider={selectedLLMProvider}
onSearchChange={handleSearch}
onProviderClick={handleProviderSelection}
/>
<div className="w-[60%] h-full px-2 flex flex-col gap-y-2">
<div className="w-[60%] h-full p-[18px] flex flex-col gap-2.5">
<div className="flex flex-col gap-[15px]">
<div className="flex flex-col gap-1.5">
<p className="text-sm font-medium text-white light:text-slate-800">
{t("chat_window.workspace_llm_manager.available_models", {
provider: providerName,
})}
</p>
<p className="text-xs font-medium text-zinc-400 light:text-slate-500">
{t(
"chat_window.workspace_llm_manager.available_models_description"
)}
</p>
</div>
{!missingCredentials && (
<ChatModelSelection
provider={selectedLLMProvider}
setHasChanges={setHasChanges}
selectedLLMModel={selectedLLMModel}
setSelectedLLMModel={setSelectedLLMModel}
/>
)}
</div>
<NoSetupWarning
showing={missingCredentials}
onSetupClick={() => {
@ -128,18 +162,12 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
);
}}
/>
<ChatModelSelection
provider={selectedLLMProvider}
setHasChanges={setHasChanges}
selectedLLMModel={selectedLLMModel}
setSelectedLLMModel={setSelectedLLMModel}
/>
{hasChanges && (
{hasChanges && !missingCredentials && (
<button
type="button"
disabled={saving}
onClick={handleSave}
className={`border-none text-xs px-4 py-1 font-semibold light:text-[#ffffff] rounded-lg bg-primary-button hover:bg-secondary hover:text-white h-[34px] whitespace-nowrap w-full`}
className="border-none text-xs px-4 py-1.5 font-semibold rounded-lg bg-white text-zinc-900 hover:bg-zinc-200 light:bg-slate-800 light:text-white light:hover:bg-slate-700 h-8 w-full cursor-pointer transition-colors mt-auto"
>
{saving
? t("chat_window.workspace_llm_manager.saving")

View File

@ -1,246 +0,0 @@
import { useEffect, useState, useRef } from "react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import AddPresetModal from "./AddPresetModal";
import EditPresetModal from "./EditPresetModal";
import { useModal } from "@/hooks/useModal";
import System from "@/models/system";
import { DotsThree, Plus } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import PublishEntityModal from "@/components/CommunityHub/PublishEntityModal";
export const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
const {
isOpen: isAddModalOpen,
openModal: openAddModal,
closeModal: closeAddModal,
} = useModal();
const {
isOpen: isEditModalOpen,
openModal: openEditModal,
closeModal: closeEditModal,
} = useModal();
const {
isOpen: isPublishModalOpen,
openModal: openPublishModal,
closeModal: closePublishModal,
} = useModal();
const [presets, setPresets] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null);
const [presetToPublish, setPresetToPublish] = useState(null);
const [searchParams] = useSearchParams();
useEffect(() => {
fetchPresets();
}, []);
/*
* @checklist-item
* If the URL has the slash-commands param, open the add modal for the user
* automatically when the component mounts.
*/
useEffect(() => {
if (
searchParams.get("action") === "open-new-slash-command-modal" &&
!isAddModalOpen
)
openAddModal();
}, []);
if (isActiveAgentSession) return null;
const fetchPresets = async () => {
const presets = await System.getSlashCommandPresets();
setPresets(presets);
};
const handleSavePreset = async (preset) => {
const { error } = await System.createSlashCommandPreset(preset);
if (!!error) {
showToast(error, "error");
return false;
}
fetchPresets();
closeAddModal();
return true;
};
const handleEditPreset = (preset) => {
setSelectedPreset(preset);
openEditModal();
};
const handleUpdatePreset = async (updatedPreset) => {
const { error } = await System.updateSlashCommandPreset(
updatedPreset.id,
updatedPreset
);
if (!!error) {
showToast(error, "error");
return;
}
fetchPresets();
closeEditModalAndResetPreset();
};
const handleDeletePreset = async (presetId) => {
await System.deleteSlashCommandPreset(presetId);
fetchPresets();
closeEditModalAndResetPreset();
};
const closeEditModalAndResetPreset = () => {
closeEditModal();
setSelectedPreset(null);
};
const handlePublishPreset = (preset) => {
setPresetToPublish({
name: preset.command.slice(1),
description: preset.description,
command: preset.command,
prompt: preset.prompt,
});
openPublishModal();
};
return (
<>
{presets.map((preset) => (
<PresetItem
key={preset.id}
preset={preset}
onUse={() => {
setShowing(false);
sendCommand({ text: `${preset.command} ` });
promptRef?.current?.focus();
}}
onEdit={handleEditPreset}
onPublish={handlePublishPreset}
/>
))}
<button
onClick={openAddModal}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-1 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-row flex pointer-events-none items-center gap-2">
<Plus size={24} weight="fill" className="text-theme-text-primary" />
<div className="text-theme-text-primary text-sm font-medium">
{t("chat_window.add_new_preset")}
</div>
</div>
</button>
<AddPresetModal
isOpen={isAddModalOpen}
onClose={closeAddModal}
onSave={handleSavePreset}
/>
{selectedPreset && (
<EditPresetModal
isOpen={isEditModalOpen}
onClose={closeEditModalAndResetPreset}
onSave={handleUpdatePreset}
onDelete={handleDeletePreset}
preset={selectedPreset}
/>
)}
<PublishEntityModal
show={isPublishModalOpen}
onClose={closePublishModal}
entityType="slash-command"
entity={presetToPublish}
/>
</>
);
}
function PresetItem({ preset, onUse, onEdit, onPublish }) {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef(null);
const menuButtonRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (
showMenu &&
menuRef.current &&
!menuRef.current.contains(event.target) &&
menuButtonRef.current &&
!menuButtonRef.current.contains(event.target)
) {
setShowMenu(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [showMenu]);
return (
<button
type="button"
data-slash-command={preset.command}
onClick={onUse}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-row justify-start items-center relative"
>
<div className="flex-col text-left flex pointer-events-none flex-1 min-w-0">
<div className="text-theme-text-primary text-sm font-bold truncate">
{preset.command}
</div>
<div className="text-theme-text-secondary text-sm truncate">
{preset.description}
</div>
</div>
<button
ref={menuButtonRef}
type="button"
tabIndex={-1}
onClick={(e) => {
e.stopPropagation();
setShowMenu(!showMenu);
}}
className="border-none text-theme-text-primary text-sm p-1 hover:cursor-pointer hover:bg-theme-action-menu-item-hover rounded-full ml-2 flex-shrink-0 z-20"
aria-label="More actions"
>
<DotsThree size={24} weight="bold" />
</button>
{showMenu && (
<div
ref={menuRef}
className="absolute right-0 top-10 bg-theme-bg-popup-menu rounded-lg z-50 min-w-[160px] shadow-lg border border-theme-modal-border flex flex-col"
>
<button
type="button"
className="px-[10px] py-[6px] text-sm text-white hover:bg-theme-sidebar-item-hover rounded-t-lg cursor-pointer border-none w-full text-left whitespace-nowrap"
onClick={(e) => {
e.stopPropagation();
setShowMenu(false);
onEdit(preset);
}}
>
Edit
</button>
<button
type="button"
className="px-[10px] py-[6px] text-sm text-white hover:bg-theme-sidebar-item-hover rounded-b-lg cursor-pointer border-none w-full text-left whitespace-nowrap"
onClick={(e) => {
e.stopPropagation();
setShowMenu(false);
onPublish(preset);
}}
>
Publish
</button>
</div>
)}
</button>
);
}

View File

@ -1,25 +0,0 @@
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function EndAgentSession({ setShowing, sendCommand }) {
const isActiveAgentSession = useIsAgentSessionActive();
if (!isActiveAgentSession) return null;
return (
<button
type="button"
data-slash-command="/exit"
onClick={() => {
setShowing(false);
sendCommand({ text: "/exit", autoSubmit: true });
}}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">/exit</div>
<div className="text-white text-opacity-60 text-sm">
Halt the current agent session.
</div>
</div>
</button>
);
}

View File

@ -1,28 +0,0 @@
export default function SlashCommandIcon(props) {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
x="1.02539"
y="1.43799"
width="17.252"
height="17.252"
rx="2"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M6.70312 14.5408L12.5996 5.8056"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}

View File

@ -1,90 +0,0 @@
import { useEffect, useRef, useState } from "react";
import SlashCommandIcon from "./icons/SlashCommandIcon";
import { Tooltip } from "react-tooltip";
import ResetCommand from "./reset";
import EndAgentSession from "./endAgentSession";
import SlashPresets from "./SlashPresets";
import { useTranslation } from "react-i18next";
import { useSlashCommandKeyboardNavigation } from "@/hooks/useSlashCommandKeyboardNavigation";
export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
const { t } = useTranslation();
return (
<div
id="slash-cmd-btn"
data-tooltip-id="tooltip-slash-cmd-btn"
data-tooltip-content={t("chat_window.slash")}
onClick={() => setShowSlashCommand(!showing)}
className={`flex justify-center items-center cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ${
showing ? "!opacity-100" : ""
}`}
>
<SlashCommandIcon
color="var(--theme-sidebar-footer-icon-fill)"
className="w-[18px] h-[18px] pointer-events-none"
/>
<Tooltip
id="tooltip-slash-cmd-btn"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}
export function SlashCommands({
showing,
setShowing,
sendCommand,
promptRef,
centered = false,
}) {
const cmdRef = useRef(null);
useSlashCommandKeyboardNavigation({ showing });
useEffect(() => {
function listenForOutsideClick() {
if (!showing || !cmdRef.current) return false;
document.addEventListener("click", closeIfOutside);
}
listenForOutsideClick();
}, [showing, cmdRef.current]);
const closeIfOutside = ({ target }) => {
if (target.id === "slash-cmd-btn") return;
const isOutside = !cmdRef?.current?.contains(target);
if (!isOutside) return;
setShowing(false);
};
return (
<div hidden={!showing}>
<div
className={
centered
? "w-full flex justify-center md:justify-start absolute top-full mt-2 left-0 z-10 px-4 md:px-0 md:pl-[31px]"
: "flex justify-center md:justify-start absolute bottom-[130px] md:bottom-[150px] left-0 right-0 z-10 max-w-[750px] mx-auto px-4 md:px-0 md:pl-[31px]"
}
>
<div
ref={cmdRef}
className="w-[600px] bg-theme-action-menu-bg rounded-2xl flex shadow flex-col justify-start items-start gap-2.5 p-2 overflow-y-auto max-h-[200px] no-scroll"
>
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
<SlashPresets
sendCommand={sendCommand}
setShowing={setShowing}
promptRef={promptRef}
/>
</div>
</div>
</div>
);
}
export function useSlashCommands() {
const [showSlashCommand, setShowSlashCommand] = useState(false);
return { showSlashCommand, setShowSlashCommand };
}

View File

@ -1,29 +0,0 @@
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import { useTranslation } from "react-i18next";
export default function ResetCommand({ setShowing, sendCommand }) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
if (isActiveAgentSession) return null; // cannot reset during active agent chat
return (
<button
type="button"
data-slash-command="/reset"
onClick={() => {
setShowing(false);
sendCommand({ text: "/reset", autoSubmit: true });
}}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">
{t("chat_window.slash_reset")}
</div>
<div className="text-white text-opacity-60 text-sm">
{t("chat_window.preset_reset_description")}
</div>
</div>
</button>
);
}

View File

@ -125,15 +125,17 @@ export default function SpeechToText({ sendCommand }) {
data-tooltip-content={`${t("chat_window.microphone")} (CTRL + M)`}
aria-label={t("chat_window.microphone")}
onClick={listening ? endSTTSession : startSTTSession}
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer ${
!!listening ? "!opacity-100" : ""
className={`group border-none relative flex justify-center items-center cursor-pointer w-8 h-8 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200 ${
listening ? "bg-zinc-700 light:bg-slate-200" : ""
}`}
>
<Microphone
weight="regular"
color="var(--theme-sidebar-footer-icon-fill)"
className={`w-[20px] h-[20px] pointer-events-none text-theme-text-primary ${
listening ? "animate-pulse-glow" : ""
size={18}
className={`pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0 ${
listening
? "animate-pulse-glow !text-white light:!text-slate-800"
: ""
}`}
/>
<Tooltip

View File

@ -1,8 +1,9 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { Stop } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
export default function StopGenerationButton() {
const { t } = useTranslation();
function emitHaltEvent() {
window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));
}
@ -13,14 +14,11 @@ export default function StopGenerationButton() {
type="button"
onClick={emitHaltEvent}
data-tooltip-id="stop-generation-button"
data-tooltip-content="Stop generating response"
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white hover:opacity-80 transition-opacity"
data-tooltip-content={t("chat_window.stop_generating")}
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-8 h-8 bg-white light:bg-slate-800 hover:opacity-80 transition-opacity"
aria-label="Stop generating"
>
<Stop
className="w-[12px] h-[12px] light:text-white text-black"
weight="fill"
/>
<div className="w-3.5 h-3.5 rounded-[4px] bg-zinc-800 light:bg-white" />
</button>
<Tooltip
id="stop-generation-button"

View File

@ -0,0 +1,30 @@
import Toggle from "@/components/lib/Toggle";
export default function SkillRow({
name,
enabled,
onToggle,
highlighted = false,
disabled = false,
}) {
let classNames = "flex items-center justify-between px-2 py-1 rounded";
if (highlighted) classNames += " bg-zinc-700/50 light:bg-slate-100";
else classNames += " hover:bg-zinc-700/50 light:hover:bg-slate-100";
if (disabled) classNames += " opacity-60 cursor-not-allowed";
else classNames += " cursor-pointer";
return (
<div
className={classNames}
data-tooltip-id={disabled ? "agent-skill-disabled-tooltip" : undefined}
>
<span className="text-xs text-white light:text-slate-900">{name}</span>
<Toggle
size="sm"
enabled={enabled}
onChange={onToggle}
disabled={disabled}
/>
</div>
);
}

View File

@ -0,0 +1,175 @@
import { useState, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import paths from "@/utils/paths";
import Admin from "@/models/admin";
import AgentPlugins from "@/models/experimental/agentPlugins";
import AgentFlows from "@/models/agentFlows";
import {
getDefaultSkills,
getConfigurableSkills,
} from "@/pages/Admin/Agents/skills";
import useToolsMenuItems from "../../useToolsMenuItems";
import SkillRow from "./SkillRow";
import { Wrench } from "@phosphor-icons/react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function AgentSkillsTab({
highlightedIndex = -1,
registerItemCount,
}) {
const { t } = useTranslation();
const agentSessionActive = useIsAgentSessionActive();
const defaultSkills = getDefaultSkills(t);
const configurableSkills = getConfigurableSkills(t);
const [disabledDefaults, setDisabledDefaults] = useState([]);
const [enabledConfigurable, setEnabledConfigurable] = useState([]);
const [importedSkills, setImportedSkills] = useState([]);
const [flows, setFlows] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSkillSettings();
}, []);
async function fetchSkillSettings() {
try {
const [prefs, flowsRes] = await Promise.all([
Admin.systemPreferencesByFields([
"disabled_agent_skills",
"default_agent_skills",
"imported_agent_skills",
]),
AgentFlows.listFlows(),
]);
if (prefs?.settings) {
setDisabledDefaults(prefs.settings.disabled_agent_skills ?? []);
setEnabledConfigurable(prefs.settings.default_agent_skills ?? []);
setImportedSkills(prefs.settings.imported_agent_skills ?? []);
}
if (flowsRes?.flows) setFlows(flowsRes.flows);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
function toggleItem(arr, item) {
return arr.includes(item) ? arr.filter((s) => s !== item) : [...arr, item];
}
function isSkillEnabled(key) {
return key in defaultSkills
? !disabledDefaults.includes(key)
: enabledConfigurable.includes(key);
}
async function toggleSkill(key) {
if (key in defaultSkills) {
const updated = toggleItem(disabledDefaults, key);
setDisabledDefaults(updated);
await Admin.updateSystemPreferences({
disabled_agent_skills: updated.join(","),
default_agent_skills: enabledConfigurable.join(","),
});
return;
}
const updated = toggleItem(enabledConfigurable, key);
setEnabledConfigurable(updated);
await Admin.updateSystemPreferences({
disabled_agent_skills: disabledDefaults.join(","),
default_agent_skills: updated.join(","),
});
}
async function toggleImportedSkill(skill) {
const newActive = !skill.active;
setImportedSkills((prev) =>
prev.map((s) =>
s.hubId === skill.hubId ? { ...s, active: newActive } : s
)
);
await AgentPlugins.toggleFeature(skill.hubId, newActive);
}
async function toggleFlow(flow) {
const newActive = !flow.active;
setFlows((prev) =>
prev.map((f) => (f.uuid === flow.uuid ? { ...f, active: newActive } : f))
);
await AgentFlows.toggleFlow(flow.uuid, newActive);
}
// Build list of all skill items for rendering/keyboard navigation
const items = useMemo(() => {
const list = [];
for (const [key, { title }] of Object.entries({
...defaultSkills,
...configurableSkills,
})) {
list.push({
id: key,
name: title,
enabled: isSkillEnabled(key),
onToggle: () => toggleSkill(key),
});
}
for (const skill of importedSkills) {
list.push({
id: skill.hubId,
name: skill.name,
enabled: skill.active,
onToggle: () => toggleImportedSkill(skill),
});
}
for (const flow of flows) {
list.push({
id: flow.uuid,
name: flow.name,
enabled: flow.active,
onToggle: () => toggleFlow(flow),
});
}
return list;
}, [disabledDefaults, enabledConfigurable, importedSkills, flows]);
useToolsMenuItems({
items,
highlightedIndex,
onSelect: agentSessionActive ? () => {} : (item) => item.onToggle(),
registerItemCount,
});
if (loading) return null;
return (
<>
{!agentSessionActive && (
<p className="text-xs text-theme-text-secondary text-center py-1">
{t("chat_window.use_agent_session_to_use_tools")}
</p>
)}
{items.map((item, index) => (
<SkillRow
key={item.id}
name={item.name}
enabled={item.enabled}
onToggle={item.onToggle}
highlighted={highlightedIndex === index}
disabled={agentSessionActive}
/>
))}
<Link to={paths.settings.agentSkills()}>
<button className="flex items-center gap-1.5 px-2 h-6 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 text-theme-text-primary">
<Wrench size={12} className="text-theme-text-primary" />
<span className="text-xs text-theme-text-primary">
{t("chat_window.manage_agent_skills")}
</span>
</button>
</Link>
</>
);
}

View File

@ -0,0 +1,100 @@
import { useState, useRef, useEffect } from "react";
import { DotsThree } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
export default function SlashCommandRow({
command,
description,
onClick,
onEdit,
onPublish,
showMenu = false,
highlighted = false,
}) {
const { t } = useTranslation();
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef(null);
const menuBtnRef = useRef(null);
useEffect(() => {
if (!menuOpen) return;
function handleClickOutside(e) {
if (
menuRef.current &&
!menuRef.current.contains(e.target) &&
menuBtnRef.current &&
!menuBtnRef.current.contains(e.target)
) {
setMenuOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [menuOpen]);
return (
<div
onClick={onClick}
className={`flex items-center justify-between px-2 py-1 rounded cursor-pointer group relative ${
highlighted
? "bg-zinc-700/50 light:bg-slate-100"
: "hover:bg-zinc-700/50 light:hover:bg-slate-100"
}`}
>
<div className="flex gap-1.5 items-center text-xs min-w-0 flex-1">
<span className="text-white light:text-slate-900 shrink-0">
{command}
</span>
<span className="text-zinc-400 light:text-slate-500 italic truncate">
{description}
</span>
</div>
{showMenu && (
<div className="relative shrink-0 ml-1">
<button
ref={menuBtnRef}
type="button"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(!menuOpen);
}}
className="border-none cursor-pointer text-zinc-400 light:text-slate-500 p-0.5 hover:text-white light:hover:text-slate-900 rounded opacity-0 group-hover:opacity-100"
>
<DotsThree size={16} weight="bold" />
</button>
{menuOpen && (
<div
ref={menuRef}
className="absolute right-0 top-full z-50 bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg shadow-lg min-w-[120px] flex flex-col overflow-hidden"
>
<button
type="button"
className="border-none px-3 py-1.5 text-xs text-white light:text-slate-900 hover:bg-zinc-700 light:hover:bg-slate-100 cursor-pointer text-left"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
onEdit?.();
}}
>
{t("chat_window.edit")}
</button>
<button
type="button"
className="border-none px-3 py-1.5 text-xs text-white light:text-slate-900 hover:bg-zinc-700 light:hover:bg-slate-100 cursor-pointer text-left"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
onPublish?.();
}}
>
{t("chat_window.publish")}
</button>
</div>
)}
</div>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from ".";
import { CMD_REGEX } from "./constants";
import { useTranslation } from "react-i18next";
export default function AddPresetModal({ isOpen, onClose, onSave }) {

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from ".";
import { CMD_REGEX } from "./constants";
export default function EditPresetModal({
isOpen,

View File

@ -0,0 +1 @@
export const CMD_REGEX = /[^a-zA-Z0-9_-]/g;

View File

@ -0,0 +1,234 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { Plus } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import System from "@/models/system";
import { useModal } from "@/hooks/useModal";
import AddPresetModal from "./SlashPresets/AddPresetModal";
import EditPresetModal from "./SlashPresets/EditPresetModal";
import PublishEntityModal from "@/components/CommunityHub/PublishEntityModal";
import showToast from "@/utils/toast";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import { PROMPT_INPUT_EVENT } from "@/components/WorkspaceChat/ChatContainer/PromptInput";
import useToolsMenuItems from "../../useToolsMenuItems";
import SlashCommandRow from "./SlashCommandRow";
export default function SlashCommandsTab({
sendCommand,
setShowing,
promptRef,
highlightedIndex = -1,
registerItemCount,
}) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
const {
isOpen: isAddModalOpen,
openModal: openAddModal,
closeModal: closeAddModal,
} = useModal();
const {
isOpen: isEditModalOpen,
openModal: openEditModal,
closeModal: closeEditModal,
} = useModal();
const {
isOpen: isPublishModalOpen,
openModal: openPublishModal,
closeModal: closePublishModal,
} = useModal();
const [presets, setPresets] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null);
const [presetToPublish, setPresetToPublish] = useState(null);
useEffect(() => {
fetchPresets();
}, []);
const fetchPresets = async () => {
const presets = await System.getSlashCommandPresets();
setPresets(presets);
};
// Build the list of selectable items for keyboard navigation and rendering
// Command names must stay as static English strings since the backend
// matches against exact "/reset" and "/exit" commands.
const items = useMemo(() => {
const builtIn = isActiveAgentSession
? {
command: "/exit",
description: t("chat_window.preset_exit_description"),
autoSubmit: true,
}
: {
command: "/reset",
description: t("chat_window.preset_reset_description"),
autoSubmit: true,
};
return [
builtIn,
...presets.map((preset) => ({
command: preset.command,
description: preset.description,
autoSubmit: false,
preset,
})),
];
}, [isActiveAgentSession, presets]);
const handleUseCommand = useCallback(
(command, autoSubmit = false) => {
setShowing(false);
// Auto-submit commands (/reset, /exit) fire immediately
if (autoSubmit) {
sendCommand({ text: command, autoSubmit: true });
promptRef?.current?.focus();
return;
}
// Insert the command at the cursor, replacing a trailing "/" if present
const textarea = promptRef?.current;
if (!textarea) return;
const cursor = textarea.selectionStart;
const value = textarea.value;
const charBefore = cursor > 0 ? value[cursor - 1] : "";
const insertStart = charBefore === "/" ? cursor - 1 : cursor;
const newValue =
value.slice(0, insertStart) + command + value.slice(cursor);
window.dispatchEvent(
new CustomEvent(PROMPT_INPUT_EVENT, {
detail: { messageContent: newValue },
})
);
textarea.focus();
const newCursor = insertStart + command.length;
setTimeout(() => textarea.setSelectionRange(newCursor, newCursor), 0);
},
[sendCommand, setShowing, promptRef]
);
useToolsMenuItems({
items,
highlightedIndex,
onSelect: (item) => {
const text = item.preset ? `${item.command} ` : item.command;
handleUseCommand(text, item.autoSubmit);
},
registerItemCount,
});
const handleSavePreset = async (preset) => {
const { error } = await System.createSlashCommandPreset(preset);
if (error) {
showToast(error, "error");
return false;
}
fetchPresets();
closeAddModal();
return true;
};
const handleEditPreset = (preset) => {
setSelectedPreset(preset);
openEditModal();
};
const handleUpdatePreset = async (updatedPreset) => {
const { error } = await System.updateSlashCommandPreset(
updatedPreset.id,
updatedPreset
);
if (error) {
showToast(error, "error");
return;
}
fetchPresets();
closeEditModal();
setSelectedPreset(null);
};
const handleDeletePreset = async (presetId) => {
await System.deleteSlashCommandPreset(presetId);
fetchPresets();
closeEditModal();
setSelectedPreset(null);
};
const handlePublishPreset = (preset) => {
setPresetToPublish({
name: preset.command.slice(1),
description: preset.description,
command: preset.command,
prompt: preset.prompt,
});
openPublishModal();
};
return (
<>
{items.map((item, index) => (
<SlashCommandRow
key={item.preset?.id ?? item.command}
command={item.command}
description={item.description}
onClick={() =>
handleUseCommand(
item.preset ? `${item.command} ` : item.command,
item.autoSubmit
)
}
onEdit={item.preset ? () => handleEditPreset(item.preset) : undefined}
onPublish={
item.preset ? () => handlePublishPreset(item.preset) : undefined
}
showMenu={!!item.preset}
highlighted={highlightedIndex === index}
/>
))}
{/* Add new */}
{!isActiveAgentSession && (
<div
onClick={openAddModal}
className="flex items-center gap-1.5 px-2 py-1 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100"
>
<Plus
size={12}
weight="bold"
className="text-white light:text-slate-900"
/>
<span className="text-xs text-white light:text-slate-900">
{t("chat_window.add_new")}
</span>
</div>
)}
{/* Modals */}
<AddPresetModal
isOpen={isAddModalOpen}
onClose={closeAddModal}
onSave={handleSavePreset}
/>
{selectedPreset && (
<EditPresetModal
isOpen={isEditModalOpen}
onClose={() => {
closeEditModal();
setSelectedPreset(null);
}}
onSave={handleUpdatePreset}
onDelete={handleDeletePreset}
preset={selectedPreset}
/>
)}
<PublishEntityModal
show={isPublishModalOpen}
onClose={closePublishModal}
entityType="slash-command"
entity={presetToPublish}
/>
</>
);
}

View File

@ -0,0 +1,171 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useUser from "@/hooks/useUser";
import AgentSkillsTab from "./Tabs/AgentSkills";
import SlashCommandsTab from "./Tabs/SlashCommands";
export const TOOLS_MENU_KEYBOARD_EVENT = "tools-menu-keyboard";
function getTabs(t, user) {
const tabs = [
{
key: "slash-commands",
label: t("chat_window.slash_commands"),
component: SlashCommandsTab,
},
];
// Only show agent skills tab for admins or when multiuser mode is off
const canSeeAgentSkills =
!user?.hasOwnProperty("role") || user.role === "admin";
if (canSeeAgentSkills) {
tabs.push({
key: "agent-skills",
label: t("chat_window.agent_skills"),
component: AgentSkillsTab,
});
}
return tabs;
}
/**
* @param {boolean} props.showing
* @param {function} props.setShowing
* @param {function} props.sendCommand
* @param {object} props.promptRef
* @param {boolean} [props.centered] - when true, popup opens below the input
*/
export default function ToolsMenu({
showing,
setShowing,
sendCommand,
promptRef,
centered = false,
highlightedIndexRef,
}) {
const { t } = useTranslation();
const { user } = useUser();
const TABS = useMemo(() => getTabs(t, user), [t, user]);
const [activeTab, setActiveTab] = useState(TABS[0].key);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const itemCountRef = useRef(0);
// Always open to the slash commands
useEffect(() => {
if (showing) setActiveTab(TABS[0].key);
}, [showing]);
// Reset highlight when switching tabs or closing
useEffect(() => {
setHighlightedIndex(-1);
}, [activeTab, showing]);
// Keep the parent ref in sync so PromptInput can check it on Enter
useEffect(() => {
if (highlightedIndexRef) highlightedIndexRef.current = highlightedIndex;
}, [highlightedIndex]);
const registerItemCount = useCallback((count) => {
itemCountRef.current = count;
}, []);
useEffect(() => {
if (!showing) return;
function handleKeyboard(e) {
const { key } = e.detail;
if (key === "ArrowLeft" || key === "ArrowRight") {
const currentIdx = TABS.findIndex((tab) => tab.key === activeTab);
const nextIdx =
key === "ArrowLeft"
? (currentIdx - 1 + TABS.length) % TABS.length
: (currentIdx + 1) % TABS.length;
setActiveTab(TABS[nextIdx].key);
return;
}
if (key === "ArrowUp" || key === "ArrowDown") {
const count = itemCountRef.current;
if (count === 0) return;
setHighlightedIndex((prev) => {
if (key === "ArrowDown") {
return prev < count - 1 ? prev + 1 : 0;
}
return prev > 0 ? prev - 1 : count - 1;
});
return;
}
// Enter is handled by the tab components via highlightedIndex
}
window.addEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleKeyboard);
return () =>
window.removeEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleKeyboard);
}, [showing, activeTab]);
if (!showing) return null;
const { component: ActiveTab } = TABS.find((tab) => tab.key === activeTab);
return (
<>
<div
className="fixed inset-0 z-40"
onMouseDown={(e) => e.preventDefault()}
onClick={() => setShowing(false)}
/>
<div
onMouseDown={(e) => {
// Prevents prompt textarea from losing focus when clicking inside the menu.
// Skip for portaled modals so their inputs can still receive focus.
if (e.currentTarget.contains(e.target)) e.preventDefault();
}}
className={`absolute left-2 right-2 md:left-14 md:right-auto md:w-[400px] z-50 bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg p-3 flex flex-col gap-2.5 shadow-lg overflow-hidden ${
centered
? "top-full mt-2 max-h-[min(360px,calc(100dvh-25rem))]"
: "bottom-full mb-2 max-h-[min(360px,calc(100dvh-11rem))]"
}`}
>
<div className="flex shrink-0 gap-2.5 items-center">
{TABS.map((tab) => (
<TabButton
key={tab.key}
active={activeTab === tab.key}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</TabButton>
))}
</div>
<div className="flex flex-col gap-1 overflow-y-auto no-scroll flex-1 min-h-0">
<ActiveTab
sendCommand={sendCommand}
setShowing={setShowing}
promptRef={promptRef}
highlightedIndex={highlightedIndex}
registerItemCount={registerItemCount}
/>
</div>
</div>
</>
);
}
function TabButton({ active, onClick, children }) {
return (
<button
type="button"
onClick={onClick}
className={`border-none cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 px-1.5 py-0.5 rounded text-[10px] font-medium text-center whitespace-nowrap ${
active
? "bg-zinc-700 text-white light:bg-slate-200 light:text-slate-800"
: "text-zinc-400 light:text-slate-800"
}`}
>
{children}
</button>
);
}

View File

@ -0,0 +1,32 @@
import { useEffect } from "react";
import { TOOLS_MENU_KEYBOARD_EVENT } from "./";
/**
* Shared hook for ToolsMenu tabs that registers the item count
* for Up/Down navigation and handles Enter to select the highlighted item.
* @param {Array} items - the list of items rendered in the tab
* @param {number} highlightedIndex - currently highlighted index from parent
* @param {function} onSelect - called with the highlighted item on Enter
* @param {function} registerItemCount - callback to register total item count with parent
*/
export default function useToolsMenuItems({
items,
highlightedIndex,
onSelect,
registerItemCount,
}) {
useEffect(() => {
registerItemCount?.(items.length);
}, [items.length, registerItemCount]);
useEffect(() => {
if (highlightedIndex < 0 || highlightedIndex >= items.length) return;
function handleEnter(e) {
if (e.detail.key !== "Enter") return;
onSelect(items[highlightedIndex]);
}
window.addEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleEnter);
return () =>
window.removeEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleEnter);
}, [highlightedIndex, items, onSelect]);
}

View File

@ -1,17 +1,7 @@
import React, { useState, useRef, useEffect } from "react";
import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
} from "./SlashCommands";
import { useState, useRef, useEffect } from "react";
import debounce from "lodash.debounce";
import { ArrowUp } from "@phosphor-icons/react";
import { ArrowUp, At } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton";
import AvailableAgentsButton, {
AvailableAgents,
useAvailableAgents,
} from "./AgentMenu";
import TextSizeButton from "./TextSizeMenu";
import LLMSelectorAction from "./LLMSelector/action";
import SpeechToText from "./SpeechToText";
import { Tooltip } from "react-tooltip";
import AttachmentManager from "./Attachments";
@ -25,6 +15,9 @@ import useTextSize from "@/hooks/useTextSize";
import { useTranslation } from "react-i18next";
import Appearance from "@/models/appearance";
import usePromptInputStorage from "@/hooks/usePromptInputStorage";
import ToolsMenu, { TOOLS_MENU_KEYBOARD_EVENT } from "./ToolsMenu";
import { useSearchParams } from "react-router-dom";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export const PROMPT_INPUT_ID = "primary-prompt-input";
export const PROMPT_INPUT_EVENT = "set_prompt_input";
@ -50,15 +43,18 @@ export default function PromptInput({
}) {
const { t } = useTranslation();
const { isDisabled } = useIsDisabled();
const agentSessionActive = useIsAgentSessionActive();
const [promptInput, setPromptInput] = useState("");
const { showAgents, setShowAgents } = useAvailableAgents();
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const [showTools, setShowTools] = useState(false);
const autoOpenedToolsRef = useRef(false);
const toolsHighlightRef = useRef(-1);
const formRef = useRef(null);
const textareaRef = useRef(null);
const [_, setFocused] = useState(false);
const undoStack = useRef([]);
const redoStack = useRef([]);
const { textSizeClass } = useTextSize();
const [searchParams] = useSearchParams();
// Synchronizes prompt input value with localStorage, scoped to the current thread.
usePromptInputStorage({
@ -66,6 +62,18 @@ export default function PromptInput({
setPromptInput,
});
/*
* @checklist-item
* If the URL has the agent param, open the agent menu for the user
* automatically when the component mounts.
*/
useEffect(() => {
if (searchParams.get("action") === "set-agent-chat") {
sendCommand({ text: "@agent " });
textareaRef.current?.focus();
}
}, [textareaRef.current]);
/**
* To prevent too many re-renders we remotely listen for updates from the parent
* via an event cycle. Otherwise, using message as a prop leads to a re-render every
@ -75,6 +83,8 @@ export default function PromptInput({
function handlePromptUpdate(e) {
const { messageContent, writeMode = "replace" } = e?.detail ?? {};
if (writeMode === "append") setPromptInput((prev) => prev + messageContent);
else if (writeMode === "prepend")
setPromptInput((prev) => messageContent + " " + prev);
else setPromptInput(messageContent ?? "");
}
@ -106,7 +116,10 @@ export default function PromptInput({
const debouncedSaveState = debounce(saveCurrentState, 250);
function handleSubmit(e) {
// Ignore submits from portaled modals (slash command preset forms)
if (e.target !== e.currentTarget) return;
setFocused(false);
setShowTools(false);
submit(e);
}
@ -115,31 +128,63 @@ export default function PromptInput({
textareaRef.current.style.height = "auto";
}
function checkForSlash(e) {
const input = e.target.value;
if (input === "/") setShowSlashCommand(true);
if (showSlashCommand) setShowSlashCommand(false);
return;
}
const watchForSlash = debounce(checkForSlash, 300);
function checkForAt(e) {
const input = e.target.value;
if (input === "@") return setShowAgents(true);
if (showAgents) return setShowAgents(false);
}
const watchForAt = debounce(checkForAt, 300);
/**
* Capture enter key press to handle submission, redo, or undo
* via keyboard shortcuts
* @param {KeyboardEvent} event
*/
function captureEnterOrUndo(event) {
// Forward keyboard events to the ToolsMenu when open
if (showTools) {
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)
) {
event.preventDefault();
window.dispatchEvent(
new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {
detail: { key: event.key },
})
);
return;
}
// When an item is highlighted via arrow keys, Enter selects it.
// Otherwise, Enter falls through to submit the form normally.
if (event.key === "Enter" && toolsHighlightRef.current >= 0) {
event.preventDefault();
window.dispatchEvent(
new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {
detail: { key: "Enter" },
})
);
return;
}
if (event.key === "Escape") {
event.preventDefault();
setShowTools(false);
textareaRef.current?.focus();
return;
}
}
// "/" toggles the Tools menu only when the input is empty
if (
event.key === "/" &&
!event.ctrlKey &&
!event.metaKey &&
promptInput.trim() === ""
) {
setShowTools((prev) => {
autoOpenedToolsRef.current = !prev;
return !prev;
});
return;
}
// Is simple enter key press w/o shift key
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled
setShowTools(false);
return submit(event);
}
@ -252,10 +297,15 @@ export default function PromptInput({
function handleChange(e) {
debouncedSaveState(-1);
watchForSlash(e);
watchForAt(e);
adjustTextArea(e);
setPromptInput(e.target.value);
const value = e.target.value;
setPromptInput(value);
// Auto-dismiss the tools menu when the "/" that opened it is modified
if (autoOpenedToolsRef.current && showTools && value !== "/") {
setShowTools(false);
autoOpenedToolsRef.current = false;
}
}
return (
@ -263,23 +313,9 @@ export default function PromptInput({
className={
centered
? "w-full relative flex justify-center items-center"
: "w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center pwa:pb-5"
: "w-full fixed md:absolute bottom-0 left-0 z-10 flex justify-center items-center pwa:pb-5"
}
>
<SlashCommands
showing={showSlashCommand}
setShowing={setShowSlashCommand}
sendCommand={sendCommand}
promptRef={textareaRef}
centered={centered}
/>
<AvailableAgents
showing={showAgents}
setShowing={setShowAgents}
sendCommand={sendCommand}
promptRef={textareaRef}
centered={centered}
/>
<form
onSubmit={handleSubmit}
className={
@ -291,80 +327,72 @@ export default function PromptInput({
<div
className={`flex items-center rounded-lg md:w-full ${centered ? "mb-0" : "mb-4"}`}
>
<div className="w-[95vw] md:w-[750px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-[20px] pwa:rounded-3xl flex flex-col px-2 overflow-hidden">
<AttachmentManager attachments={attachments} />
<div className="flex items-center mx-[7px]">
<textarea
id={PROMPT_INPUT_ID}
ref={textareaRef}
onChange={handleChange}
onKeyDown={captureEnterOrUndo}
onPaste={(e) => {
saveCurrentState();
handlePasteEvent(e);
}}
required={true}
onFocus={() => setFocused(true)}
onBlur={(e) => {
setFocused(false);
adjustTextArea(e);
}}
value={promptInput}
spellCheck={Appearance.get("enableSpellCheck")}
className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 pt-[12px] w-full leading-5 text-white bg-transparent placeholder:text-white/60 light:placeholder:text-theme-text-primary resize-none active:outline-none focus:outline-none flex-grow mb-1 pwa:!text-[16px] ${textSizeClass}`}
placeholder={t("chat_window.send_message")}
/>
</div>
<div className="flex justify-between items-center pt-3.5 pb-3 mx-[7px]">
<div className="flex gap-x-2 items-center h-5 -ml-[4.5px]">
<AttachItem
workspaceSlug={workspaceSlug}
workspaceThreadSlug={threadSlug}
<div className="relative w-[95vw] md:w-[750px]">
<ToolsMenu
showing={showTools}
setShowing={setShowTools}
sendCommand={sendCommand}
promptRef={textareaRef}
centered={centered}
highlightedIndexRef={toolsHighlightRef}
/>
<div className="bg-zinc-800 light:bg-white light:border light:border-slate-300 rounded-[20px] pwa:rounded-3xl flex flex-col px-5 overflow-hidden">
<AttachmentManager attachments={attachments} />
<div className="flex items-center">
<textarea
id={PROMPT_INPUT_ID}
ref={textareaRef}
onChange={handleChange}
onKeyDown={captureEnterOrUndo}
onPaste={(e) => {
saveCurrentState();
handlePasteEvent(e);
}}
required={true}
onFocus={() => setFocused(true)}
onBlur={(e) => {
setFocused(false);
adjustTextArea(e);
}}
value={promptInput}
spellCheck={Appearance.get("enableSpellCheck")}
className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] pt-[20px] w-full leading-5 text-white light:text-slate-600 bg-transparent placeholder:text-white/60 light:placeholder:text-slate-400 resize-none active:outline-none focus:outline-none flex-grow pwa:!text-[16px] ${textSizeClass}`}
placeholder={t("chat_window.send_message")}
/>
<SlashCommandsButton
showing={showSlashCommand}
setShowSlashCommand={setShowSlashCommand}
/>
<AvailableAgentsButton
showing={showAgents}
setShowAgents={setShowAgents}
/>
<TextSizeButton />
<LLMSelectorAction workspaceSlug={workspaceSlug} />
</div>
<div className="flex gap-x-2 items-center h-5">
<SpeechToText sendCommand={sendCommand} />
{isStreaming ? (
<StopGenerationButton />
) : (
<>
<button
ref={formRef}
type="submit"
disabled={isDisabled}
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white disabled:cursor-not-allowed disabled:opacity-50 hover:opacity-80 transition-opacity"
data-tooltip-id="send-prompt"
data-tooltip-content={
isDisabled
? t("chat_window.attachments_processing")
: t("chat_window.send")
}
aria-label={t("chat_window.send")}
>
<ArrowUp
className="w-[12px] h-[12px] pointer-events-none light:text-white text-black"
weight="bold"
/>
<span className="sr-only">Send message</span>
</button>
<Tooltip
id="send-prompt"
place="bottom"
delayShow={300}
className="tooltip !text-xs z-99"
<div className="flex justify-between items-center pt-3.5 pb-3">
<div className="flex items-center gap-x-0.25">
<div className="flex items-center gap-x-1">
<AttachItem
workspaceSlug={workspaceSlug}
workspaceThreadSlug={threadSlug}
/>
</>
)}
<AgentSessionButton
sendCommand={sendCommand}
promptInput={promptInput}
textareaRef={textareaRef}
visible={!agentSessionActive}
/>
</div>
<ToolsButton
showTools={showTools}
setShowTools={setShowTools}
textareaRef={textareaRef}
autoOpenedToolsRef={autoOpenedToolsRef}
/>
</div>
<div className="flex gap-x-2 items-center">
<SpeechToText sendCommand={sendCommand} />
{isStreaming ? (
<StopGenerationButton />
) : (
<SendPromptButton
formRef={formRef}
promptInput={promptInput}
isDisabled={isDisabled}
/>
)}
</div>
</div>
</div>
</div>
@ -374,6 +402,123 @@ export default function PromptInput({
);
}
function AgentSessionButton({
sendCommand,
promptInput,
textareaRef,
visible = true,
}) {
const { t } = useTranslation();
if (!visible) return null;
function handleClick() {
try {
if (promptInput?.trim()?.startsWith("@agent")) return;
sendCommand({ text: "@agent", writeMode: "prepend" });
} finally {
textareaRef?.current?.focus();
}
}
return (
<>
<button
type="button"
onClick={handleClick}
data-tooltip-id="agent-session"
data-tooltip-content={t("chat_window.start_agent_session")}
aria-label={t("chat_window.start_agent_session")}
className="group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200"
>
<At
size={18}
className="pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0"
/>
</button>
<Tooltip
id="agent-session"
place="bottom"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</>
);
}
function ToolsButton({
showTools,
setShowTools,
textareaRef,
autoOpenedToolsRef,
}) {
const { t } = useTranslation();
return (
<button
id="tools-btn"
type="button"
onClick={() => {
autoOpenedToolsRef.current = false;
setShowTools(!showTools);
textareaRef.current?.focus();
}}
className={`group border-none cursor-pointer flex items-center justify-center h-6 px-2 rounded-full ${
showTools
? "bg-zinc-700 light:bg-slate-200"
: "hover:bg-zinc-700 light:hover:bg-slate-200"
}`}
>
<span
className={`text-sm font-medium ${
showTools
? "text-white light:text-slate-800"
: "text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-800"
}`}
>
{t("chat_window.tools")}
</span>
</button>
);
}
function SendPromptButton({ formRef, promptInput, isDisabled }) {
const { t } = useTranslation();
return (
<>
<button
ref={formRef}
type="submit"
disabled={isDisabled || !promptInput.trim().length}
className={`border-none flex justify-center items-center rounded-full w-8 h-8 transition-all ${
promptInput.trim().length && !isDisabled
? "cursor-pointer bg-white hover:bg-zinc-200 light:bg-slate-800 light:hover:bg-slate-600"
: "cursor-not-allowed bg-zinc-600 light:bg-slate-400"
}`}
data-tooltip-id="send-prompt"
data-tooltip-content={
isDisabled
? t("chat_window.attachments_processing")
: t("chat_window.send")
}
aria-label={t("chat_window.send")}
>
<ArrowUp
className="w-[18px] h-[18px] pointer-events-none text-zinc-800 light:text-white"
weight="bold"
/>
<span className="sr-only">{t("chat_window.send")}</span>
</button>
<Tooltip
id="send-prompt"
place="bottom"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</>
);
}
/**
* Handle event listeners to prevent the send button from being used
* for whatever reason that may we may want to prevent the user from sending a message.

View File

@ -0,0 +1,56 @@
import { Fragment } from "react";
import { CaretLeft, Info, X } from "@phosphor-icons/react";
import { decode as HTMLDecode } from "he";
import truncate from "truncate";
import { useTranslation } from "react-i18next";
import { omitChunkHeader } from "../../../ChatHistory/Citation";
import { toPercentString } from "@/utils/numbers";
export default function SourceDetailView({ source, onBack, onClose }) {
const { t } = useTranslation();
return (
<>
<div className="flex items-center justify-between">
<button
onClick={onBack}
type="button"
className="text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors"
>
<CaretLeft size={20} weight="bold" />
</button>
<p className="font-semibold text-base leading-6 text-white light:text-slate-900 truncate px-2">
{truncate(source.title, 30)}
</p>
<button
onClick={onClose}
type="button"
className="text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors"
>
<X size={16} weight="bold" />
</button>
</div>
<div className="flex flex-col overflow-y-auto no-scroll">
{source.chunks.map(({ text, score }, idx) => (
<Fragment key={idx}>
<div className="flex flex-col gap-y-1 py-4">
<p className="text-sm leading-[20px] text-white light:text-slate-900">
{HTMLDecode(omitChunkHeader(text))}
</p>
{!!score && (
<div className="flex items-center text-xs text-white/60 light:text-slate-500 gap-x-1">
<Info size={14} />
<p>
{toPercentString(score)} {t("chat_window.similarity_match")}
</p>
</div>
)}
</div>
{idx !== source.chunks.length - 1 && (
<hr className="border-zinc-700 light:border-slate-300" />
)}
</Fragment>
))}
</div>
</>
);
}

View File

@ -0,0 +1,56 @@
import { X } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import ModalWrapper from "@/components/ModalWrapper";
import { combineLikeSources } from "../../ChatHistory/Citation";
import SourceDetailView from "./SourceDetailView";
import SourceItem from "../SourceItem";
export default function MobileCitationModal({
sources: rawSources,
isOpen,
selectedSource,
setSelectedSource,
onClose,
}) {
const sources = combineLikeSources(rawSources);
const { t } = useTranslation();
return (
<ModalWrapper isOpen={isOpen}>
<div className="fixed inset-0" onClick={onClose} />
<div className="relative z-10 w-[calc(100%-40px)] max-h-[70vh] rounded-[16px] bg-zinc-800 light:bg-white light:border-2 light:border-slate-300 p-4 flex flex-col gap-4">
{selectedSource ? (
<SourceDetailView
source={selectedSource}
onBack={() => setSelectedSource(null)}
onClose={onClose}
/>
) : (
<>
<div className="flex items-center justify-between">
<p className="font-semibold text-base leading-6 text-white light:text-slate-900">
{t("chat_window.sources")}
</p>
<button
onClick={onClose}
type="button"
className="text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors"
>
<X size={16} weight="bold" />
</button>
</div>
<div className="flex flex-col gap-3 overflow-y-auto no-scroll">
{sources.map((source, idx) => (
<SourceItem
key={source.title || idx}
source={source}
onClick={() => setSelectedSource(source)}
/>
))}
</div>
</>
)}
</div>
</ModalWrapper>
);
}

View File

@ -0,0 +1,27 @@
import { parseChunkSource, SourceTypeCircle } from "../../ChatHistory/Citation";
import { useTranslation } from "react-i18next";
export default function SourceItem({ source, onClick }) {
const { t } = useTranslation();
const info = parseChunkSource(source);
const subtitle = info.isUrl ? info.text : t("chat_window.document");
return (
<button
type="button"
onClick={onClick}
className="flex flex-col gap-[2px] items-start w-full text-left hover:opacity-75 transition-opacity"
>
<div className="flex gap-[6px] items-start w-full">
<SourceTypeCircle type={info.icon} size={16} iconSize={10} />
<p className="flex-1 font-medium text-sm text-white light:text-slate-900 leading-[15px] truncate">
{source.title}
</p>
</div>
<div className="flex flex-col gap-[2px] pl-[22px] text-[10px] text-zinc-400 light:text-slate-500 leading-[14px]">
<p>{subtitle}</p>
<p>{t("chat_window.source_count", { count: source.references })}</p>
</div>
</button>
);
}

View File

@ -0,0 +1,103 @@
import { createContext, useContext, useState } from "react";
import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { X } from "@phosphor-icons/react";
import {
combineLikeSources,
CitationDetailModal,
} from "../ChatHistory/Citation";
import MobileCitationModal from "./MobileCitationModal";
import SourceItem from "./SourceItem";
export const SourcesSidebarContext = createContext();
export function SourcesSidebarProvider({ children }) {
const [sources, setSources] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(false);
function openSidebar(newSources) {
setSources(newSources);
setSidebarOpen(true);
}
function closeSidebar() {
setSidebarOpen(false);
}
return (
<SourcesSidebarContext.Provider
value={{ sources, sidebarOpen, openSidebar, closeSidebar }}
>
{children}
</SourcesSidebarContext.Provider>
);
}
export function useSourcesSidebar() {
return useContext(SourcesSidebarContext);
}
export default function SourcesSidebar() {
const { sources, sidebarOpen, closeSidebar } = useSourcesSidebar();
const { t } = useTranslation();
const [selectedSource, setSelectedSource] = useState(null);
const combined = combineLikeSources(sources);
if (isMobile) {
return (
<MobileCitationModal
sources={sources}
isOpen={sidebarOpen}
selectedSource={selectedSource}
setSelectedSource={setSelectedSource}
onClose={() => {
setSelectedSource(null);
closeSidebar();
}}
/>
);
}
return (
<>
<div
className="h-full overflow-hidden transition-all duration-500 flex-shrink-0"
style={{ width: sidebarOpen ? "366px" : "0px" }}
>
<div
className="ml-4 w-[350px] bg-zinc-900 light:bg-white light:border-2 light:border-slate-300 md:rounded-[16px] p-4 flex flex-col gap-4 overflow-hidden mt-[72px]"
style={{ maxHeight: "calc(100% - 88px)" }}
>
<div className="flex items-start justify-between">
<p className="font-medium text-base leading-6 text-white light:text-slate-900">
{t("chat_window.sources")}
</p>
<button
onClick={closeSidebar}
type="button"
className="text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors"
>
<X size={16} weight="bold" />
</button>
</div>
<div className="flex flex-col gap-3 overflow-y-auto no-scroll">
{combined.map((source, idx) => (
<SourceItem
key={source.title || idx}
source={source}
onClick={() => setSelectedSource(source)}
/>
))}
</div>
</div>
</div>
{selectedSource && (
<CitationDetailModal
source={selectedSource}
onClose={() => setSelectedSource(null)}
/>
)}
</>
);
}

View File

@ -0,0 +1,101 @@
import { useState, useRef, useEffect, useMemo } from "react";
import { SlidersHorizontal } from "@phosphor-icons/react";
import useLoginMode from "@/hooks/useLoginMode";
import { useTranslation } from "react-i18next";
function getTextSizes(t) {
return [
{ key: "small", label: t("chat_window.small"), textClass: "text-xs" },
{ key: "normal", label: t("chat_window.normal"), textClass: "text-sm" },
{ key: "large", label: t("chat_window.large"), textClass: "text-base" },
];
}
export default function TextSizeMenu() {
const { t } = useTranslation();
const TEXT_SIZES = useMemo(() => getTextSizes(t), [t]);
const mode = useLoginMode();
const [showMenu, setShowMenu] = useState(false);
const [selectedSize, setSelectedSize] = useState(
window.localStorage.getItem("anythingllm_text_size") || "normal"
);
const menuRef = useRef(null);
const buttonRef = useRef(null);
useEffect(() => {
if (!showMenu) return;
function handleClickOutside(e) {
if (
menuRef.current &&
!menuRef.current.contains(e.target) &&
buttonRef.current &&
!buttonRef.current.contains(e.target)
) {
setShowMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [showMenu]);
function handleTextSizeChange(size) {
setSelectedSize(size);
window.localStorage.setItem("anythingllm_text_size", size);
window.dispatchEvent(new CustomEvent("textSizeChange", { detail: size }));
}
// User icon is visible when login mode is active (single with password or multi-user)
const hasUserIcon = mode !== null;
return (
<div
className={`absolute top-3 md:top-5 z-30 ${hasUserIcon ? "right-[55px] md:right-[67px]" : "right-4 md:right-6"}`}
>
<button
ref={buttonRef}
type="button"
onClick={() => setShowMenu(!showMenu)}
className={`group border-none cursor-pointer flex items-center justify-center w-[35px] h-[35px] rounded-full transition-all ${
showMenu
? "bg-zinc-700 light:bg-slate-200"
: "hover:bg-zinc-700 light:hover:bg-slate-200"
}`}
>
<SlidersHorizontal
size={18}
className={
showMenu
? "text-white light:text-slate-800"
: "text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-800"
}
/>
</button>
{showMenu && (
<div
ref={menuRef}
className="absolute right-0 top-[42px] bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg p-3 w-[200px] flex flex-col gap-1 shadow-lg"
>
<p className="text-[10px] font-medium text-zinc-400 light:text-slate-500 px-2 mb-0.5">
{t("chat_window.text_size_label")}
</p>
{TEXT_SIZES.map(({ key, label, textClass }) => (
<div
key={key}
onClick={() => handleTextSizeChange(key)}
className={`flex items-center px-2 py-1 rounded cursor-pointer ${
selectedSize === key
? "bg-zinc-700 light:bg-slate-200"
: "hover:bg-zinc-700/50 light:hover:bg-slate-100"
}`}
>
<span className={`${textClass} text-white light:text-slate-900`}>
{label}
</span>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,136 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import useUser from "@/hooks/useUser";
import { useModal } from "@/hooks/useModal";
import LLMSelectorModal from "../PromptInput/LLMSelector/index";
import SetupProvider from "../PromptInput/LLMSelector/SetupProvider";
import {
SAVE_LLM_SELECTOR_EVENT,
PROVIDER_SETUP_EVENT,
} from "../PromptInput/LLMSelector/action";
import Workspace from "@/models/workspace";
import System from "@/models/system";
import { SIDEBAR_TOGGLE_EVENT } from "@/components/Sidebar/SidebarToggle";
function fetchModelName(slug, setModelName) {
if (!slug) return;
Promise.all([Workspace.bySlug(slug), System.keys()]).then(
([workspace, systemSettings]) => {
const model = workspace.chatModel ?? systemSettings?.LLMModel ?? "";
setModelName(model);
}
);
}
export default function WorkspaceModelPicker({ workspaceSlug = null }) {
const { t } = useTranslation();
const { slug: urlSlug } = useParams();
const slug = urlSlug ?? workspaceSlug;
const { user } = useUser();
const [showSelector, setShowSelector] = useState(false);
const [modelName, setModelName] = useState("");
const {
isOpen: isSetupProviderOpen,
openModal: openSetupProviderModal,
closeModal: closeSetupProviderModal,
} = useModal();
const [config, setConfig] = useState({ settings: {}, provider: null });
const [refreshKey, setRefreshKey] = useState(0);
const [sidebarOpen, setSidebarOpen] = useState(
() => window.localStorage.getItem("anythingllm_sidebar_toggle") !== "closed"
);
useEffect(() => {
const handleToggle = (e) => setSidebarOpen(e.detail.open);
window.addEventListener(SIDEBAR_TOGGLE_EVENT, handleToggle);
return () => window.removeEventListener(SIDEBAR_TOGGLE_EVENT, handleToggle);
}, []);
// Fetch current model name for display
useEffect(() => fetchModelName(slug, setModelName), [slug]);
// Close selector and refresh model name when model is saved
useEffect(() => {
function handleSave() {
setShowSelector(false);
fetchModelName(slug, setModelName);
}
window.addEventListener(SAVE_LLM_SELECTOR_EVENT, handleSave);
return () =>
window.removeEventListener(SAVE_LLM_SELECTOR_EVENT, handleSave);
}, [slug]);
// Handle provider setup request
useEffect(() => {
function handleProviderSetup(e) {
const { provider, settings } = e.detail;
setConfig({ settings, provider });
setTimeout(() => openSetupProviderModal(), 300);
}
window.addEventListener(PROVIDER_SETUP_EVENT, handleProviderSetup);
return () =>
window.removeEventListener(PROVIDER_SETUP_EVENT, handleProviderSetup);
}, []);
// This feature is disabled for multi-user instances where the user is not an admin
if (!!user && user.role !== "admin") return null;
if (!slug) return null;
return (
<>
{showSelector && (
<div
className="fixed inset-0 z-20"
onClick={() => setShowSelector(false)}
/>
)}
<div
className={`hidden md:block absolute top-2 z-30 transition-all duration-500 ${
sidebarOpen ? "left-3" : "left-11"
}`}
>
<button
type="button"
onClick={() => setShowSelector(!showSelector)}
className={`group border-none cursor-pointer px-2.5 py-1 flex items-center rounded-full transition-all ${
showSelector
? "bg-zinc-700 light:bg-slate-200"
: "hover:bg-zinc-700 light:hover:bg-slate-200"
}`}
>
<span
className={`text-xs ${
showSelector
? "text-white light:text-slate-800"
: "text-zinc-500 light:text-slate-500 group-hover:text-white light:group-hover:text-slate-800"
}`}
>
{modelName || t("chat_window.select_model")}
</span>
</button>
{showSelector && (
<div className="absolute left-0 top-full mt-1 bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-xl shadow-lg w-[620px] overflow-hidden">
<LLMSelectorModal
key={refreshKey}
workspaceSlug={slug}
initialProvider={config.provider?.value}
/>
</div>
)}
</div>
<SetupProvider
isOpen={isSetupProviderOpen}
closeModal={closeSetupProviderModal}
postSubmit={() => {
closeSetupProviderModal();
setRefreshKey((k) => k + 1);
}}
settings={config.settings}
llmProvider={config.provider}
/>
</>
);
}

View File

@ -15,6 +15,7 @@ import handleSocketResponse, {
websocketURI,
AGENT_SESSION_END,
AGENT_SESSION_START,
setAgentSessionActive,
} from "@/utils/chat/agent";
import DnDFileUploaderWrapper from "./DnDWrapper";
import SpeechRecognition, {
@ -24,11 +25,15 @@ import { ChatTooltips } from "./ChatTooltips";
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
import { clearPromptInputDraft } from "@/hooks/usePromptInputStorage";
import { safeJsonParse } from "@/utils/request";
import { useTranslation } from "react-i18next";
import paths from "@/utils/paths";
import QuickActions from "@/components/lib/QuickActions";
import SuggestedMessages from "@/components/lib/SuggestedMessages";
import TextSizeMenu from "./TextSizeMenu";
import WorkspaceModelPicker from "./WorkspaceModelPicker";
import SourcesSidebar, { SourcesSidebarProvider } from "./SourcesSidebar";
export default function ChatContainer({ workspace, knownHistory = [] }) {
const navigate = useNavigate();
@ -66,6 +71,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
document.getElementById(PROMPT_INPUT_ID)?.value || "";
if (!currentMessage) return false;
// Clear the localStorage draft for this thread/workspace so that if the
// PromptInput remounts (emptychat transition), it won't restore stale text
clearPromptInputDraft(threadSlug ?? workspace.slug);
const prevChatHistory = [
...chatHistory,
{
@ -118,7 +127,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
* @param {boolean} options.autoSubmit - Determines if the text should be sent immediately or if it should be added to the message state (default: false)
* @param {Object[]} options.history - The history of the chat prior to this message for overriding the current chat history
* @param {Object[import("./DnDWrapper").Attachment]} options.attachments - The attachments to send to the LLM for this message
* @param {'replace' | 'append'} options.writeMode - Replace current text or append to existing text (default: replace)
* @param {'replace' | 'append' | 'prepend'} options.writeMode - Replace current text or append to existing text (default: replace)
* @returns {void}
*/
const sendCommand = async ({
@ -134,6 +143,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
return;
}
if (writeMode === "prepend") {
const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
text = currentText + " " + text;
}
// If we are auto-submitting in append mode
// than we need to update text with whatever is in the prompt input + the text we are sending.
// @note: `message` will not work here since it is not updated yet.
@ -144,6 +158,12 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
}
if (!text || text === "") return false;
// Clear the localStorage draft so that if the PromptInput remounts
// (e.g. /reset causing emptychat or chatempty transitions),
// it won't restore stale text.
clearPromptInputDraft(threadSlug ?? workspace.slug);
// If we are auto-submitting
// Then we can replace the current text since this is not accumulating.
let prevChatHistory;
@ -250,17 +270,20 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
// TODO: Simplify this WSS stuff
useEffect(() => {
let socket = null;
function handleWSS() {
try {
if (!socketId || !!websocket) return;
const socket = new WebSocket(
socket = new WebSocket(
`${websocketURI()}/api/agent-invocation/${socketId}`
);
socket.supportsAgentStreaming = false;
window.addEventListener(ABORT_STREAM_EVENT, () => {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
websocket.close();
socket?.close();
});
socket.addEventListener("message", (event) => {
@ -269,6 +292,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
handleSocketResponse(socket, event, setChatHistory);
} catch {
console.error("Failed to parse data");
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
socket.close();
}
@ -276,6 +300,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
});
socket.addEventListener("close", (_event) => {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content),
@ -296,6 +321,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setSocketId(null);
});
setWebsocket(socket);
setAgentSessionActive(true);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
} catch (e) {
@ -319,6 +345,14 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
}
}
handleWSS();
return () => {
if (socket) {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
socket.close();
}
};
}, [socketId]);
const isEmpty =
@ -328,9 +362,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border"
>
{isMobile && <SidebarMobileHeader />}
<TextSizeMenu />
<WorkspaceModelPicker workspaceSlug={workspace.slug} />
<DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full items-center justify-center">
<div className="flex flex-col items-center w-full max-w-[750px]">
@ -369,35 +405,42 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
}
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll no-scroll z-[2]"
>
{isMobile && <SidebarMobileHeader />}
<DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full">
<div className="contents">
<MetricsProvider>
<ChatHistory
ref={chatHistoryRef}
history={chatHistory}
workspace={workspace}
sendCommand={sendCommand}
updateHistory={setChatHistory}
regenerateAssistantMessage={regenerateAssistantMessage}
/>
</MetricsProvider>
<PromptInput
submit={handleSubmit}
isStreaming={loadingResponse}
sendCommand={sendCommand}
attachments={files}
centered={false}
/>
</div>
<SourcesSidebarProvider>
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative flex md:ml-[2px] md:mr-[16px] md:my-[16px] w-full h-full z-[2]"
>
<TextSizeMenu />
<div className="flex-1 min-w-0 transition-all duration-500 relative md:rounded-[16px] bg-zinc-900 light:bg-white text-white light:text-slate-900 h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border">
{isMobile && <SidebarMobileHeader />}
<WorkspaceModelPicker workspaceSlug={workspace.slug} />
<DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full pb-20 md:pb-0">
<div className="contents">
<MetricsProvider>
<ChatHistory
ref={chatHistoryRef}
history={chatHistory}
workspace={workspace}
sendCommand={sendCommand}
updateHistory={setChatHistory}
regenerateAssistantMessage={regenerateAssistantMessage}
/>
</MetricsProvider>
<PromptInput
submit={handleSubmit}
isStreaming={loadingResponse}
sendCommand={sendCommand}
attachments={files}
centered={false}
/>
</div>
</div>
</DnDFileUploaderWrapper>
<ChatTooltips />
</div>
</DnDFileUploaderWrapper>
<ChatTooltips />
</div>
<SourcesSidebar />
</div>
</SourcesSidebarProvider>
);
}

View File

@ -1,30 +0,0 @@
import { useState, useEffect, useCallback } from "react";
const ALIGNMENT_STORAGE_KEY = "anythingllm-chat-message-alignment";
/**
* Store the message alignment in localStorage as well as provide a function to get the alignment of a message via role.
* @returns {{msgDirection: 'left'|'left_right', setMsgDirection: (direction: string) => void, getMessageAlignment: (role: string) => string}} - The message direction and the class name for the direction.
*/
export function useChatMessageAlignment() {
const [msgDirection, setMsgDirection] = useState(
() => localStorage.getItem(ALIGNMENT_STORAGE_KEY) ?? "left"
);
useEffect(() => {
if (msgDirection) localStorage.setItem(ALIGNMENT_STORAGE_KEY, msgDirection);
}, [msgDirection]);
const getMessageAlignment = useCallback(
(role) => {
const isLeftToRight = role === "user" && msgDirection === "left_right";
return isLeftToRight ? "flex-row-reverse" : "";
},
[msgDirection]
);
return {
msgDirection,
setMsgDirection,
getMessageAlignment,
};
}

View File

@ -24,6 +24,20 @@ import { safeJsonParse } from "@/utils/request";
* @param {Function} props.setPromptInput - State setter function for prompt input
* @returns {void}
*/
/**
* Immediately clears the stored draft for a given thread/workspace key.
* Used before state updates that may remount PromptInput to prevent
* stale text from being restored.
* @param {string} storageKey - thread slug or workspace slug
*/
export function clearPromptInputDraft(storageKey) {
try {
const map = safeJsonParse(localStorage.getItem(USER_PROMPT_INPUT_MAP), {});
map[storageKey] = "";
localStorage.setItem(USER_PROMPT_INPUT_MAP, JSON.stringify(map));
} catch {}
}
export default function usePromptInputStorage({ promptInput, setPromptInput }) {
const { threadSlug = null, slug: workspaceSlug } = useParams();
useEffect(() => {

View File

@ -1,62 +0,0 @@
import { useEffect, useRef } from "react";
/**
* Handles keyboard navigation for the slash commands menu is presented in the UI.
* @param {boolean} showing - Whether the slash commands menu is showing
* @returns {void}
*/
export function useSlashCommandKeyboardNavigation({ showing }) {
const focusedCommandRef = useRef(null);
const availableCommands = useRef([]);
useEffect(() => {
const commands = document.querySelectorAll("[data-slash-command]");
availableCommands.current = Array.from(commands).map(
(cmd) => cmd.dataset.slashCommand
);
}, [showing]);
useEffect(() => {
if (!showing) return;
document.addEventListener("keydown", handleKeyboardNavigation);
return () =>
document.removeEventListener("keydown", handleKeyboardNavigation);
}, [showing]);
useEffect(() => {
// Reset the focused command when the slash commands menu is closed or opened
focusedCommandRef.current = null;
}, [showing]);
function handleKeyboardNavigation(event) {
event.preventDefault();
if (!availableCommands.current.length) return;
let currentIndex = availableCommands.current.indexOf(
focusedCommandRef.current
);
// If the enter key is pressed, click the focused command if it exists
// This will also trigger the onClick event of the focused command
// to cleanup everything on hide
if (event.key === "Enter" && !!focusedCommandRef.current) {
document
.querySelector(`[data-slash-command="${focusedCommandRef.current}"]`)
?.click();
return;
}
// If the current index is -1, set it to the last command, otherwise inc/dec by 1
if (currentIndex === -1)
currentIndex = availableCommands.current.length - 1;
else currentIndex += event.key === "ArrowUp" ? -1 : 1;
// Wrap around the array both ways if index is out of bounds
if (currentIndex < 0) currentIndex = availableCommands.current.length - 1;
else if (currentIndex >= availableCommands.current.length) currentIndex = 0;
focusedCommandRef.current = availableCommands.current[currentIndex];
document
.querySelector(`[data-slash-command="${focusedCommandRef.current}"]`)
?.focus();
}
}

View File

@ -326,25 +326,6 @@ a {
}
}
.doc__source {
opacity: 0;
animation-delay: 50ms;
animation: citationAnimation 0.15s ease-out 0s forwards;
}
@keyframes citationAnimation {
0% {
opacity: 0;
}
80% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
@media (prefers-color-scheme: light) {
.sidebar-items:after {
@ -677,7 +658,7 @@ dialog::backdrop {
}
.tooltip {
@apply !bg-black !text-white !py-2 !px-3 !rounded-md;
@apply !bg-black !text-white !py-2 !px-3 !rounded-md !z-10;
}
.Toastify__toast-body {

View File

@ -148,12 +148,6 @@ const TRANSLATIONS = {
heading: "اشرح لي",
body: "فوائد برنامج إيني ثينك إلْلْمْ",
},
pfp: {
title: "صورة الملف الشخصي للمساعد",
description: "تخصيص صورة الملف الشخصي للمساعد لمساحة العمل هذه.",
image: "صورة مساحة العمل",
remove: "إزالة صورة مساحة العمل",
},
delete: {
title: "حذف مساحة العمل",
description:
@ -648,8 +642,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "أرسل رسالة",
attach_file: "أرفق ملفًا بهذا الدردشة",
slash: "عرض جميع الأوامر المتاحة للتواصل.",
agents: "عرض جميع الوكلاء المتاحين الذين يمكنك استخدامهم للمحادثة.",
text_size: "تغيير حجم النص.",
microphone: "اذكر طلبك.",
send: "أرسل رسالة فورية إلى مساحة العمل",
@ -660,18 +652,11 @@ const TRANSLATIONS = {
regenerate_response: "أعد الرد",
good_response: "رد جيد",
more_actions: "إجراءات إضافية",
hide_citations: "إخفاء المراجع",
show_citations: "عرض المراجع",
fork: "شوكة",
delete: "حذف",
save_submit: "حفظ وإرسال",
cancel: "إلغاء",
edit_prompt: "اقتراح التحرير",
edit_response: "عدّل الرد",
at_agent: "@agent",
default_agent_description: "- الوكيل الافتراضي لهذا المساحة.",
custom_agents_coming_soon: "سيصل وكلاء مخصصون قريباً!",
slash_reset: "/reset",
preset_reset_description: "امسح سجل الدردشة الخاص بك وابدأ محادثة جديدة",
add_new_preset: "إضافة إعداد مسبق",
command: "أمر",
@ -693,6 +678,35 @@ const TRANSLATIONS = {
missing_credentials: "هذا المزود لا يمتلك المؤهلات اللازمة!",
missing_credentials_description: "انقر لإعداد بيانات الاعتماد",
},
submit: "إرسال",
edit_info_user:
'"إرسال" يعيد إنشاء استجابة الذكاء الاصطناعي. "حفظ" يقوم بتحديث رسالتك فقط.',
edit_info_assistant: "سيتم حفظ التغييرات مباشرة في هذا الرد.",
see_less: "اقرأ المزيد",
see_more: "عرض المزيد",
tools: "الأدوات",
browse: "تصفح",
text_size_label: "حجم النص",
select_model: "اختر الطراز",
sources: "مصادر",
document: "وثيقة",
similarity_match: "مباراة",
source_count_one: "{{count}}، المرجع",
source_count_other: "{{count}} المرجع",
preset_exit_description: "إيقاف الجلسة الحالية للمتصفح",
add_new: "أضف جديدًا",
edit: "تحرير",
publish: "نشر",
stop_generating: "توقف عن إنشاء رد",
pause_tts_speech_message: "توقف عن قراءة النص بصوت مسجل.",
slash_commands: "أوامر مختصرة",
agent_skills: "مهارات الوكيل",
manage_agent_skills: "إدارة مهارات الوكلاء",
agent_skills_disabled_in_session:
'لا يمكن تعديل المهارات أثناء جلسة مع عامل. يجب عليك أولاً استخدام الأمر "/exit" لإنهاء الجلسة.',
start_agent_session: "ابدأ جلسة الممثل",
use_agent_session_to_use_tools:
"يمكنك استخدام الأدوات المتاحة في الدردشة عن طريق بدء جلسة مع ممثل خدمة العملاء باستخدام الرمز '@agent' في بداية رسالتك.",
},
profile_settings: {
edit_account: "تحرير الحساب",
@ -758,10 +772,6 @@ const TRANSLATIONS = {
title: "اسم",
description: "حدد اسمًا يظهر في صفحة تسجيل الدخول لجميع المستخدمين.",
},
"chat-message-alignment": {
title: "مواءمة رسائل الدردشة",
description: "حدد وضع محاذاة الرسائل عند استخدام واجهة الدردشة.",
},
"display-language": {
title: "اللغة المعروضة",
description:

View File

@ -164,13 +164,6 @@ const TRANSLATIONS = {
heading: "Vysvětlit mi",
body: "výhody AnythingLLM",
},
pfp: {
title: "Profilový obrázek asistenta",
description:
"Přizpůsobte profilový obrázek asistenta pro tento pracovní prostor.",
image: "Obrázek pracovního prostoru",
remove: "Odebrat obrázek pracovního prostoru",
},
delete: {
title: "Smazat pracovní prostor",
description:
@ -398,11 +391,6 @@ const TRANSLATIONS = {
description:
"Nastavte název, který je zobrazen na přihlašovací stránce všem uživatelům.",
},
"chat-message-alignment": {
title: "Zarovnání zpráv chatu",
description:
"Vyberte režim zarovnání zpráv při použití rozhraní chatu.",
},
"display-language": {
title: "Zobrazovací jazyk",
description:
@ -794,9 +782,6 @@ const TRANSLATIONS = {
attachments_processing: "Přílohy se zpracovávají. Prosím čekejte...",
send_message: "Odeslat zprávu",
attach_file: "Přiložit soubor k tomuto chatu",
slash: "Zobrazit všechny dostupné lomítkové příkazy pro chatování.",
agents:
"Zobrazit všechny dostupné agenty, které můžete použít pro chatování.",
text_size: "Změnit velikost textu.",
microphone: "Mluvit svou výzvu.",
send: "Odeslat zprávu výzvy do pracovního prostoru",
@ -806,18 +791,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerovat odpověď",
good_response: "Dobrá odpověď",
more_actions: "Další akce",
hide_citations: "Skrýt citace",
show_citations: "Zobrazit citace",
fork: "Rozdělit",
delete: "Smazat",
save_submit: "Uložit a odeslat",
cancel: "Zrušit",
edit_prompt: "Upravit výzvu",
edit_response: "Upravit odpověď",
at_agent: "@agent",
default_agent_description: " - výchozí agent pro tento pracovní prostor.",
custom_agents_coming_soon: "vlastní agenti přicházejí brzy!",
slash_reset: "/reset",
preset_reset_description: "Vymazat historii chatu a začít nový chat",
add_new_preset: " Přidat novou předvolbu",
command: "Příkaz",
@ -841,6 +819,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Klikněte pro nastavení přihlašovacích údajů",
},
submit: "Odeslat",
edit_info_user:
"„Odeslat“ znovu vygeneruje odpověď od AI. „Uložit“ aktualizuje pouze vaši zprávu.",
edit_info_assistant: "Vaše změny budou uloženy přímo v tomto odpovědi.",
see_less: "Zobrazit méně",
see_more: "Více",
tools: "Nářadí",
browse: "Prohlédněte si",
text_size_label: "Velikost písma",
select_model: "Vyberte model",
sources: "Zdroje",
document: "Dokument",
similarity_match: "zápas",
source_count_one: "{{count}} odkaz",
source_count_other: "{{count}} odkazy",
preset_exit_description: "Zastavte aktuální relaci s agentem",
add_new: "Přidat nové",
edit: "Upravit",
publish: "Publikovat",
stop_generating: "Zastavte generování odpovědi",
pause_tts_speech_message:
"Zastavte čtení textu pomocí syntetické řeči z tohoto zprávy.",
slash_commands: "Příkazy v řádku",
agent_skills: "Dovednosti agenta",
manage_agent_skills: "Řízení dovedností agentů",
agent_skills_disabled_in_session:
"Není možné upravovat dovednosti během aktivního sezení s agentem. Nejprve použijte příkaz `/exit` pro ukončení sezení.",
start_agent_session: "Spustit relaci s agentem",
use_agent_session_to_use_tools:
"Můžete využít nástroje v chatu spuštěním sezení s agentem pomocí příkazu '@agent' na začátku vašeho vstupu.",
},
profile_settings: {
edit_account: "Upravit účet",

View File

@ -150,12 +150,6 @@ const TRANSLATIONS = {
heading: "Forklar mig",
body: "fordelene ved AnythingLLM",
},
pfp: {
title: "Assistentens profilbillede",
description: "Tilpas assistentens profilbillede for dette arbejdsområde.",
image: "Arbejdsområdebillede",
remove: "Fjern arbejdsområdebillede",
},
delete: {
title: "Slet arbejdsområde",
description:
@ -656,8 +650,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Send en besked",
attach_file: "Vedhæft en fil til denne chat",
slash: "Vis alle tilgængelige skråstreg-kommandoer til chat.",
agents: "Vis alle tilgængelige agenter, du kan bruge til chat.",
text_size: "Ændr tekststørrelse.",
microphone: "Tal din prompt.",
send: "Send promptbesked til arbejdsområdet",
@ -669,18 +661,11 @@ const TRANSLATIONS = {
regenerate_response: "Genopbyg svar",
good_response: "Godt svar",
more_actions: "Flere handlinger",
hide_citations: "Skjul henvisninger",
show_citations: "Vis henvisninger",
fork: "Fork",
delete: "Slet",
save_submit: "Gem og indsende",
cancel: "Annullér",
edit_prompt: "Redigeringsanmodning",
edit_response: "Rediger svar",
at_agent: "@agent",
default_agent_description: "- standardagenten for dette arbejdsområde.",
custom_agents_coming_soon: "Specialagenter kommer snart!",
slash_reset: "/reset",
preset_reset_description:
"Rydd op i din chat-historik og start en ny samtale",
add_new_preset: "Tilføj ny forudindstilling",
@ -706,6 +691,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Klik for at oprette legitimationsoplysninger",
},
submit: "Indsend",
edit_info_user:
'"Send" genopretter AI-responsen. "Gem" opdaterer kun dit budskab.',
edit_info_assistant:
"Ændringerne, du laver, vil blive gemt direkte i dette svar.",
see_less: "Se mindre",
see_more: "Se flere",
tools: "Værktøj",
browse: "Gennemse",
text_size_label: "Tekststørrelse",
select_model: "Vælg model",
sources: "Kilder",
document: "Dokument",
similarity_match: "kamp",
source_count_one: "{{count}} henvisning",
source_count_other: "{{count}} referencer",
preset_exit_description: "Afslut den aktuelle agent-session",
add_new: "Tilføj nyt",
edit: "Rediger",
publish: "Udgive",
stop_generating: "Stop med at generere svar",
pause_tts_speech_message: "Pause TTS-læsningen af beskeden",
slash_commands: "Kommandoer",
agent_skills: "Agenters kompetencer",
manage_agent_skills: "Administrer agenters kompetencer",
agent_skills_disabled_in_session:
"Det er ikke muligt at ændre færdigheder under en aktiv agent-session. Brug kommandoen `/exit` for at afslutte sessionen først.",
start_agent_session: "Start Agent-session",
use_agent_session_to_use_tools:
"Du kan bruge værktøjer i chat ved at starte en agent-session med '@agent' i starten af din forespørgsel.",
},
profile_settings: {
edit_account: "Rediger konto",
@ -773,10 +788,6 @@ const TRANSLATIONS = {
description:
"Angiv et navn, der vises på login-siden for alle brugere.",
},
"chat-message-alignment": {
title: "Sammenstillet samtale",
description: "Vælg alignmentsmoden, når du bruger chat-grænsefladen.",
},
"display-language": {
title: "Visningssprog",
description:

View File

@ -157,13 +157,6 @@ const TRANSLATIONS = {
heading: "Erkläre mir",
body: "die Vorteile von AnythingLLM",
},
pfp: {
title: "Assistent-Profilbild",
description:
"Passen Sie das Profilbild des Assistenten für diesen Workspace an.",
image: "Workspace-Bild",
remove: "Workspace-Bild entfernen",
},
delete: {
title: "Workspace löschen",
description:
@ -392,11 +385,6 @@ const TRANSLATIONS = {
description:
"Geben Sie einen Anwendungsnamen ein, der auf der Login-Seite erscheint.",
},
"chat-message-alignment": {
title: "Nachrichtenanordnung im Chat",
description:
"Bestimmen Sie den Ausrichtungsmodus der Chat-Nachrichten.",
},
"display-language": {
title: "Sprache",
description:
@ -772,8 +760,6 @@ const TRANSLATIONS = {
attachments_processing: "Anhänge werden verarbeitet. Bitte warten...",
send_message: "Schreibe eine Nachricht",
attach_file: "Füge eine Datei zum Chat hinzu",
slash: "Schau dir alle verfügbaren Slash Befehle für den Chat an.",
agents: "Schau dir alle verfugbaren Agentenfähigkeiten für den Chat an.",
text_size: "Ändere die Größe des Textes.",
microphone: "Spreche deinen Prompt ein.",
send: "Versende den Prompt an den Workspace.",
@ -783,18 +769,11 @@ const TRANSLATIONS = {
regenerate_response: "Antwort neu generieren",
good_response: "Gute Antwort",
more_actions: "Weitere Aktionen",
hide_citations: "Quellenangaben ausblenden",
show_citations: "Quellenangaben anzeigen",
fork: "Abzweigen",
delete: "Löschen",
save_submit: "Speichern und Senden",
cancel: "Abbrechen",
edit_prompt: "Prompt bearbeiten",
edit_response: "Antwort bearbeiten",
at_agent: "@agent",
default_agent_description: " Standardagent für diesen Workspace.",
custom_agents_coming_soon: "Eigene Agenten bald verfügbar!",
slash_reset: "/reset",
preset_reset_description: "Chatverlauf löschen und neuen Chat starten",
add_new_preset: "Neues Preset anlegen",
command: "Befehl",
@ -817,6 +796,36 @@ const TRANSLATIONS = {
missing_credentials: "Für diesen Anbieter fehlen Anmeldedaten!",
missing_credentials_description: "Klicken, um Zugangsdaten einzurichten",
},
submit: "Absenden",
edit_info_user:
'"Absenden" generiert die Antwort des KI-Systems neu. "Speichern" aktualisiert lediglich Ihre Nachricht.',
edit_info_assistant:
"Ihre Änderungen werden direkt in diese Antwort gespeichert.",
see_less: "Weniger anzeigen",
see_more: "Mehr anzeigen",
tools: "Werkzeuge",
browse: "Durchsuchen",
text_size_label: "Schriftgröße",
select_model: "Modell auswählen",
sources: "Quellen",
document: "Dokument",
similarity_match: "Spiel",
source_count_one: "{{count}} Referenz",
source_count_other: "{{count}} Verweise",
preset_exit_description: "Behalte die aktuelle Agentensitzung",
add_new: "Neu hinzufügen",
edit: "Bearbeiten",
publish: "Veröffentlichen",
stop_generating: "Stoppen Sie die Generierung von Antworten",
pause_tts_speech_message: "Pause die Text-to-Speech-Funktion der Nachricht",
slash_commands: "Befehlszeilen",
agent_skills: "Fähigkeiten von Agenten",
manage_agent_skills: "Verwalten Sie die Fähigkeiten von Agenten",
agent_skills_disabled_in_session:
"Es ist nicht möglich, während einer aktiven Sitzung die Fähigkeiten zu ändern. Verwenden Sie zuerst den Befehl `/exit`, um die Sitzung zu beenden.",
start_agent_session: "Starte eine Agent-Sitzung",
use_agent_session_to_use_tools:
'Sie können Tools im Chat nutzen, indem Sie eine Agentensitzung mit "@agent" am Anfang Ihrer Anfrage starten.',
},
profile_settings: {
edit_account: "Account bearbeiten",

View File

@ -163,13 +163,6 @@ const TRANSLATIONS = {
heading: "Explain to me",
body: "the benefits of AnythingLLM",
},
pfp: {
title: "Assistant Profile Image",
description:
"Customize the profile image of the assistant for this workspace.",
image: "Workspace Image",
remove: "Remove Workspace Image",
},
delete: {
title: "Delete Workspace",
description:
@ -397,11 +390,6 @@ const TRANSLATIONS = {
description:
"Set a name that is displayed on the login page to all users.",
},
"chat-message-alignment": {
title: "Chat Message Alignment",
description:
"Select the message alignment mode when using the chat interface.",
},
"display-language": {
title: "Display Language",
description:
@ -795,8 +783,6 @@ const TRANSLATIONS = {
attachments_processing: "Attachments are processing. Please wait...",
send_message: "Send a message",
attach_file: "Attach a file to this chat",
slash: "View all available slash commands for chatting.",
agents: "View all available agents you can use for chatting.",
text_size: "Change text size.",
microphone: "Speak your prompt.",
send: "Send prompt message to workspace",
@ -806,20 +792,31 @@ const TRANSLATIONS = {
regenerate_response: "Regenerate response",
good_response: "Good response",
more_actions: "More actions",
hide_citations: "Hide citations",
show_citations: "Show citations",
sources: "Sources",
source_count_one: "{{count}} reference",
source_count_other: "{{count}} references",
document: "Document",
similarity_match: "match",
pause_tts_speech_message: "Pause TTS speech of message",
fork: "Fork",
delete: "Delete",
save_submit: "Save & Submit",
cancel: "Cancel",
submit: "Submit",
edit_prompt: "Edit prompt",
edit_response: "Edit response",
at_agent: "@agent",
default_agent_description: " - the default agent for this workspace.",
custom_agents_coming_soon: "custom agents are coming soon!",
slash_reset: "/reset",
edit_info_user:
'"Submit" regenerates the AI response. "Save" updates your message only.',
edit_info_assistant:
"Your changes will be saved directly to this response.",
see_less: "See Less",
see_more: "See More",
preset_reset_description: "Clear your chat history and begin a new chat",
preset_exit_description: "Halt the current agent session",
add_new_preset: " Add New Preset",
add_new: "Add new",
edit: "Edit",
publish: "Publish",
stop_generating: "Stop generating response",
command: "Command",
your_command: "your-command",
placeholder_prompt:
@ -830,15 +827,27 @@ const TRANSLATIONS = {
small: "Small",
normal: "Normal",
large: "Large",
tools: "Tools",
browse: "Browse",
text_size_label: "Text Size",
select_model: "Select Model",
slash_commands: "Slash Commands",
agent_skills: "Agent Skills",
manage_agent_skills: "Manage Agent Skills",
start_agent_session: "Start Agent Session",
agent_skills_disabled_in_session:
"Can't modify skills during an active agent session. Use /exit to end the session first.",
use_agent_session_to_use_tools:
"You can use tools in chat by starting an agent session with '@agent' at the beginning of your prompt.",
workspace_llm_manager: {
search: "Search LLM providers",
search: "Search",
loading_workspace_settings: "Loading workspace settings...",
available_models: "Available Models for {{provider}}",
available_models_description: "Select a model to use for this workspace.",
save: "Use this model",
saving: "Setting model as workspace default...",
missing_credentials: "This provider is missing credentials!",
missing_credentials_description: "Click to set up credentials",
missing_credentials_description: "Set up now",
},
},
profile_settings: {

View File

@ -158,13 +158,6 @@ const TRANSLATIONS = {
heading: "Explícame",
body: "los beneficios de AnythingLLM",
},
pfp: {
title: "Imagen de perfil del asistente",
description:
"Personaliza la imagen de perfil del asistente para este espacio de trabajo.",
image: "Imagen del espacio de trabajo",
remove: "Eliminar imagen del espacio de trabajo",
},
delete: {
title: "Eliminar espacio de trabajo",
description:
@ -400,11 +393,6 @@ const TRANSLATIONS = {
description:
"Establece un nombre que se mostrará en la página de inicio de sesión para todos los usuarios.",
},
"chat-message-alignment": {
title: "Alineación de mensajes de chat",
description:
"Selecciona el modo de alineación de mensajes cuando utilices la interfaz de chat.",
},
"display-language": {
title: "Idioma de visualización",
description:
@ -782,8 +770,6 @@ const TRANSLATIONS = {
"Los archivos adjuntos se están procesando. Por favor, espera...",
send_message: "Enviar un mensaje",
attach_file: "Adjuntar un archivo a este chat",
slash: "Ver todos los comandos de barra disponibles para chatear.",
agents: "Ver todos los agentes disponibles que puedes usar para chatear.",
text_size: "Cambiar tamaño del texto.",
microphone: "Habla tu prompt.",
send: "Enviar mensaje de prompt al espacio de trabajo",
@ -793,19 +779,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerar respuesta",
good_response: "Buena respuesta",
more_actions: "Más acciones",
hide_citations: "Ocultar citas",
show_citations: "Mostrar citas",
fork: "Bifurcar",
delete: "Eliminar",
save_submit: "Guardar y enviar",
cancel: "Cancelar",
edit_prompt: "Editar prompt",
edit_response: "Editar respuesta",
at_agent: "@agent",
default_agent_description:
" - el agente predeterminado para este espacio de trabajo.",
custom_agents_coming_soon: "¡los agentes personalizados llegarán pronto!",
slash_reset: "/reset",
preset_reset_description:
"Borra tu historial de chat y comienza un nuevo chat",
add_new_preset: " Agregar nuevo preajuste",
@ -833,6 +811,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Haz clic para configurar las credenciales",
},
submit: "Enviar",
edit_info_user:
'"Enviar" regenera la respuesta de la IA. "Guardar" actualiza solo tu mensaje.',
edit_info_assistant:
"Los cambios que realice se guardarán directamente en esta respuesta.",
see_less: "Ver menos",
see_more: "Ver más",
tools: "Herramientas",
browse: "Explorar",
text_size_label: "Tamaño del texto",
select_model: "Seleccionar modelo",
sources: "Fuentes",
document: "Documento",
similarity_match: "partido",
source_count_one: "{{count}} de referencia",
source_count_other: "{{count}} referencias",
preset_exit_description: "Detener la sesión actual del agente.",
add_new: "Añadir nuevo",
edit: "Editar",
publish: "Publicar",
stop_generating: "Dejar de generar respuestas",
pause_tts_speech_message: "Pausa la lectura de voz del mensaje.",
slash_commands: "Comandos abreviados",
agent_skills: "Habilidades del agente",
manage_agent_skills: "Gestionar las habilidades del agente.",
agent_skills_disabled_in_session:
"No es posible modificar las habilidades durante una sesión con un agente activo. Primero, utilice el comando `/exit` para finalizar la sesión.",
start_agent_session: "Iniciar sesión como agente",
use_agent_session_to_use_tools:
"Puede utilizar las herramientas disponibles en el chat iniciando una sesión con un agente utilizando el prefijo '@agent' al principio de su mensaje.",
},
profile_settings: {
edit_account: "Editar cuenta",

View File

@ -154,12 +154,6 @@ const TRANSLATIONS = {
heading: "Selgita mulle",
body: "AnythingLLM eeliseid",
},
pfp: {
title: "Abilise profiilipilt",
description: "Kohanda selle tööruumi abilise profiilipilti.",
image: "Tööruumi pilt",
remove: "Eemalda tööruumi pilt",
},
delete: {
title: "Kustuta tööruum",
description:
@ -378,10 +372,6 @@ const TRANSLATIONS = {
description:
"Nimi, mis kuvatakse kõigile kasutajatele sisselogimislehel.",
},
"chat-message-alignment": {
title: "Vestlussõnumite joondus",
description: "Vali sõnumite joondus vestlusliideses.",
},
"display-language": {
title: "Kuvakeel",
description:
@ -738,8 +728,6 @@ const TRANSLATIONS = {
attachments_processing: "Manused töötlevad. Palun oota…",
send_message: "Saada sõnum",
attach_file: "Lisa fail vestlusele",
slash: "Vaata kõiki slash-käske.",
agents: "Vaata kõiki agente, keda saad kasutada.",
text_size: "Muuda teksti suurust.",
microphone: "Esita päring häälega.",
send: "Saada päring tööruumi",
@ -749,18 +737,11 @@ const TRANSLATIONS = {
regenerate_response: "Loo vastus uuesti",
good_response: "Hea vastus",
more_actions: "Rohkem toiminguid",
hide_citations: "Peida viited",
show_citations: "Näita viiteid",
fork: "Hargnemine",
delete: "Kustuta",
save_submit: "Salvesta ja saada",
cancel: "Tühista",
edit_prompt: "Redigeeri päringut",
edit_response: "Redigeeri vastust",
at_agent: "@agent",
default_agent_description: " selle tööruumi vaikimisi agent.",
custom_agents_coming_soon: "kohandatud agendid tulekul!",
slash_reset: "/reset",
preset_reset_description: "Tühjenda vestlusajalugu ja alusta uut vestlust",
add_new_preset: " Lisa uus preset",
command: "Käsk",
@ -782,6 +763,35 @@ const TRANSLATIONS = {
missing_credentials: "Sellel pakkujal puuduvad võtmed!",
missing_credentials_description: "Klõpsa, et määrata võtmed",
},
submit: "Saada",
edit_info_user:
'"Saada" taastab AI vastuse. "Salvesta" muudab ainult teie sõnumi.',
edit_info_assistant: "Teie muutused salvestatakse otse sellele vastusele.",
see_less: "Näita vähem",
see_more: "Vaata rohkem",
tools: "Vahendid",
browse: "Sirva",
text_size_label: "Teksti suurus",
select_model: "Valige mudel",
sources: "Allikasid",
document: "Dokument",
similarity_match: "mäng",
source_count_one: "{{count}} viidatud",
source_count_other: "Viidatud allikad",
preset_exit_description: "Lõpeta hetkeseisuga",
add_new: "Lisada uus",
edit: "Redigeerimine",
publish: "Avaldada",
stop_generating: "Lõpeta vastuste genereerimine",
pause_tts_speech_message: "Peata sõna-sünteesi (TTS) rääkimine sõnumis",
slash_commands: "Lihtsasti kasutatavad käsud",
agent_skills: "Agentide oskused",
manage_agent_skills: "Halda agentide oskusi",
agent_skills_disabled_in_session:
"Ei ole võimalik muuta oskusi aktiivse agenti seanssi ajal. Enne seanssi lõpetamist kasutage käsku /exit.",
start_agent_session: "Alusta agenti sessiooni",
use_agent_session_to_use_tools:
"Saate kasutada vahendeid vestluses, alustades agenti sessiooni, lisades käskile '@agent' sõna.",
},
profile_settings: {
edit_account: "Muuda kontot",

View File

@ -149,12 +149,6 @@ const TRANSLATIONS = {
heading: "برایم توضیح بده",
body: "مزایای AnythingLLM را",
},
pfp: {
title: "تصویر پروفایل دستیار",
description: "تصویر پروفایل دستیار را برای این فضای کاری شخصی‌سازی کنید.",
image: "تصویر فضای کاری",
remove: "حذف تصویر فضای کاری",
},
delete: {
title: "حذف فضای کاری",
description:
@ -654,9 +648,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "یک پیام ارسال کنید",
attach_file: "لطفاً یک فایل را به این چت پیوست کنید.",
slash: "برای مشاهده تمام دستورات Slash موجود برای چت.",
agents:
"تمام عوامل موجود را که می‌توانید برای گفتگو استفاده کنید، مشاهده کنید.",
text_size: "تغییر اندازه متن.",
microphone: "سوال خود را بپرسید.",
send: "پیام فوری را برای فضای کاری ارسال کنید",
@ -667,18 +658,11 @@ const TRANSLATIONS = {
regenerate_response: "بازسازی پاسخ",
good_response: "پاسخ خوب",
more_actions: "اقدامات بیشتر",
hide_citations: "پنهان کردن ارجاعات",
show_citations: "نمایش ارجاعات",
fork: "چنگال",
delete: "حذف",
save_submit: "ذخیره و ارسال",
cancel: "ยกد",
edit_prompt: "لطفاً دستور ویرایش را ارائه دهید.",
edit_response: "لطفا پاسخ را ویرایش کنید.",
at_agent: "@agent",
default_agent_description: "- عامل پیش‌فرض برای این فضای کاری.",
custom_agents_coming_soon: "نمایندگان ویژه در حال آمدن هستند!",
slash_reset: "/reset",
preset_reset_description: "حذف تاریخچه چت خود و شروع یک چت جدید",
add_new_preset: "اضافه کردن تنظیمات پیش‌فرض جدید",
command: "دستورالعمل",
@ -703,6 +687,35 @@ const TRANSLATIONS = {
missing_credentials_description:
"برای تنظیم اعتبارها، اینجا را کلیک کنید",
},
submit: "ارسال",
edit_info_user:
'"ارسال" پاسخ تولید شده توسط هوش مصنوعی را دوباره ایجاد می‌کند. "ذخیره" فقط پیام شما را به‌روز می‌کند.',
edit_info_assistant: "تغییرات شما مستقیماً در این پاسخ ذخیره خواهند شد.",
see_less: "کمی بیشتر",
see_more: "بیشتر",
tools: "ابزارها",
browse: "جستجو",
text_size_label: "اندازه متن",
select_model: "انتخاب مدل",
sources: "منابع",
document: "اسناد",
similarity_match: "مسابقه",
source_count_one: "{{count}}، مرجع",
source_count_other: "{{count}}، منابع",
preset_exit_description: "متوقف کردن جلسه فعلی با نمایندگی",
add_new: "اضافه کردن موارد جدید",
edit: "ویرایش",
publish: "انتشار",
stop_generating: "متوقف کردن تولید پاسخ",
pause_tts_speech_message: "مکث در پخش صدای متن",
slash_commands: "دستورات کوتاه‌شده",
agent_skills: "مهارت‌های کارگزار",
manage_agent_skills: "مدیریت مهارت‌های نمایندگان",
agent_skills_disabled_in_session:
"امکان تغییر مهارت‌ها در حین یک جلسه فعال با یک عامل وجود ندارد. ابتدا با استفاده از دستور /exit، جلسه را به پایان برسانید.",
start_agent_session: "شروع جلسه با نماینده",
use_agent_session_to_use_tools:
"شما می‌توانید از ابزارهای موجود در چت با شروع یک جلسه با یک عامل از طریق استفاده از '@agent' در ابتدای پیام خود استفاده کنید.",
},
profile_settings: {
edit_account: "ویرایش حساب",
@ -768,11 +781,6 @@ const TRANSLATIONS = {
title: "نام",
description: "یک نام را برای تمام کاربران در صفحه ورود مشخص کنید.",
},
"chat-message-alignment": {
title: "همراه‌بودن پیام‌ها در چت",
description:
"هنگام استفاده از رابط چت، حالت هم‌تراز کردن پیام را انتخاب کنید.",
},
"display-language": {
title: "زبان نمایش",
description:

View File

@ -150,13 +150,6 @@ const TRANSLATIONS = {
heading: "Expliquez-moi",
body: "les avantages de AnythingLLM",
},
pfp: {
title: "Image de profil de l'assistant",
description:
"Personnalisez l'image de profil de l'assistant pour cet espace de travail.",
image: "Image de l'espace de travail",
remove: "Supprimer l'image de l'espace de travail",
},
delete: {
title: "Supprimer l'Espace de Travail",
description:
@ -656,8 +649,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Envoyer un message",
attach_file: "Joindre un fichier",
slash: "Voir les commandes slash disponibles",
agents: "Voir les agents disponibles",
text_size: "Modifier la taille du texte",
microphone: "Enregistrer un message vocal",
send: "Envoyer le message au chatbot",
@ -669,18 +660,11 @@ const TRANSLATIONS = {
regenerate_response: "Régénérer la réponse",
good_response: "Bonne réponse",
more_actions: "Plus d'actions",
hide_citations: "Masquer les citations",
show_citations: "Afficher les citations",
fork: "Dupliquer",
delete: "Supprimer",
save_submit: "Sauvegarder et envoyer",
cancel: "Annuler",
edit_prompt: "Modifier le prompt",
edit_response: "Modifier la réponse",
at_agent: "@agent",
default_agent_description: "l'agent par défaut de cet espace de travail",
custom_agents_coming_soon: "Agents personnalisés bientôt disponibles",
slash_reset: "/reset",
preset_reset_description:
"Efface l'historique du chat actuel et commence une nouvelle conversation.",
add_new_preset: "Ajouter une nouvelle commande preset",
@ -706,6 +690,37 @@ const TRANSLATIONS = {
missing_credentials_description:
"Vous devez configurer vos identifiants de fournisseur LLM avant de pouvoir sélectionner un modèle.",
},
submit: "Soumettre",
edit_info_user:
'"Soumettre" permet de régénérer la réponse de l\'IA. "Enregistrer" met uniquement à jour votre message.',
edit_info_assistant:
"Vos modifications seront enregistrées directement dans cette réponse.",
see_less: "Voir moins",
see_more: "Voir plus",
tools: "Outils",
browse: "Parcourir",
text_size_label: "Taille du texte",
select_model: "Sélectionner le modèle",
sources: "Sources",
document: "Document",
similarity_match: "match",
source_count_one: "{{count}} référence",
source_count_other: "Références à {{count}}",
preset_exit_description: "Arrêter la session actuelle de l'agent",
add_new: "Ajouter",
edit: "Modifier",
publish: "Publier",
stop_generating: "Arrêtez de générer des réponses",
pause_tts_speech_message:
"Mettre en pause la lecture de la voix synthétique du message",
slash_commands: "Commandes abrégées",
agent_skills: "Compétences des agents",
manage_agent_skills: "Gérer les compétences des agents",
agent_skills_disabled_in_session:
"Il n'est pas possible de modifier les compétences pendant une session avec un agent actif. Utilisez la commande `/exit` pour terminer la session en premier.",
start_agent_session: "Démarrer la session de l'agent",
use_agent_session_to_use_tools:
'Vous pouvez utiliser des outils via le chat en lançant une session avec un agent en utilisant le préfixe "@agent" au début de votre requête.',
},
profile_settings: {
edit_account: "Modifier le compte",
@ -773,10 +788,6 @@ const TRANSLATIONS = {
title: "Nom de l'application",
description: "Définissez le nom affiché dans l'interface.",
},
"chat-message-alignment": {
title: "Alignement des messages",
description: "Choisissez l'alignement des messages dans le chat.",
},
"display-language": {
title: "Langue d'affichage",
description: "Sélectionnez la langue de l'interface utilisateur.",

View File

@ -152,12 +152,6 @@ const TRANSLATIONS = {
heading: "הסבר לי",
body: "את היתרונות של AnythingLLM",
},
pfp: {
title: "תמונת פרופיל של העוזר",
description: "התאם אישית את תמונת הפרופיל של העוזר עבור סביבת עבודה זו.",
image: "תמונת סביבת עבודה",
remove: "הסר תמונת סביבת עבודה",
},
delete: {
title: "מחק סביבת עבודה",
description:
@ -379,10 +373,6 @@ const TRANSLATIONS = {
title: "שם",
description: "הגדר שם שיוצג בדף ההתחברות לכל המשתמשים.",
},
"chat-message-alignment": {
title: "יישור הודעות צ'אט",
description: "בחר את מצב יישור ההודעות בעת שימוש בממשק הצ'אט.",
},
"display-language": {
title: "שפת תצוגה",
description:
@ -742,8 +732,6 @@ const TRANSLATIONS = {
attachments_processing: "קבצים מצורפים בעיבוד. אנא המתן...",
send_message: "שלח הודעה",
attach_file: "צרף קובץ לצ'אט זה",
slash: "הצג את כל פקודות הסלאש הזמינות לצ'אט.",
agents: "הצג את כל הסוכנים הזמינים שתוכל להשתמש בהם לצ'אט.",
text_size: "שנה גודל טקסט.",
microphone: "אמור את ההנחיה שלך.",
send: "שלח הודעת הנחיה לסביבת העבודה",
@ -753,18 +741,11 @@ const TRANSLATIONS = {
regenerate_response: "צור תגובה מחדש",
good_response: "תגובה טובה",
more_actions: "פעולות נוספות",
hide_citations: "הסתר ציטוטים",
show_citations: "הצג ציטוטים",
fork: "פצל (Fork)",
delete: "מחק",
save_submit: "שמור ושלח",
cancel: "בטל",
edit_prompt: "ערוך הנחיה",
edit_response: "ערוך תגובה",
at_agent: "@agent",
default_agent_description: " - סוכן ברירת המחדל עבור סביבת עבודה זו.",
custom_agents_coming_soon: "סוכנים מותאמים אישית יגיעו בקרוב!",
slash_reset: "/reset",
preset_reset_description: "נקה את היסטוריית הצ'אט שלך והתחל צ'אט חדש",
add_new_preset: " הוסף הגדרה קבועה חדשה",
command: "פקודה",
@ -786,6 +767,36 @@ const TRANSLATIONS = {
missing_credentials: "חסרים אישורים לספק זה!",
missing_credentials_description: "לחץ להגדרת אישורים",
},
submit: "הגש",
edit_info_user:
'"שלח" מחזיר את התגובה של הבינה המלאכותית. "שמור" מעדכן רק את ההודעה שלך.',
edit_info_assistant: "השינויים שאתם מבצעים יישמרו ישירות בתגובה זו.",
see_less: "ראה פחות",
see_more: "לראות עוד",
tools: "כלים",
browse: "גלו",
text_size_label: "גודל הטקסט",
select_model: "בחר מודל",
sources: "מקורות",
document: "מסמך",
similarity_match: "משחק",
source_count_one: "{{count}} - הפניה",
source_count_other: "{{count}} מקורות",
preset_exit_description: "עצירת הפעולה הנוכחית של המשתמש",
add_new: "הוסף חדש",
edit: "עריכה",
publish: "להוציא לאור",
stop_generating: "הפסיקו ליצור תגובה",
pause_tts_speech_message:
"השהייה של קריאת טקסט באמצעות תוכנת TTS (Text-to-Speech)",
slash_commands: "פקודות קיצור",
agent_skills: "כישורים של סוכן",
manage_agent_skills: "ניהול מיומנויות של סוכנים",
agent_skills_disabled_in_session:
'לא ניתן לשנות כישורים במהלך סשן פעיל. יש להשתמש בפקודה "/exit" כדי לסיים את הסשן תחילה.',
start_agent_session: "התחלת סשן עם סוכן",
use_agent_session_to_use_tools:
"ניתן להשתמש בכלי הדיון באמצעות פתיחת סשן עם נציג על ידי שימוש בסימן '@agent' בתחילת ההודעה.",
},
profile_settings: {
edit_account: "ערוך חשבון",

View File

@ -151,13 +151,6 @@ const TRANSLATIONS = {
heading: "Spiegami",
body: "i vantaggi di AnythingLLM",
},
pfp: {
title: "Immagine del profilo dell'assistente",
description:
"Personalizza l'immagine del profilo dell'assistente per quest'area di lavoro.",
image: "Immagine dell'area di lavoro",
remove: "Rimuovi immagine dell'area di lavoro",
},
delete: {
title: "Elimina area di lavoro",
description:
@ -660,9 +653,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Invia un messaggio",
attach_file: "Allega un file a questa chat.",
slash: "Visualizza tutti i comandi disponibili per la chat.",
agents:
"Visualizza tutti gli agenti disponibili che puoi utilizzare per la chat.",
text_size: "Modifica la dimensione del testo.",
microphone: "Formula la tua richiesta.",
send: "Invia un messaggio immediato allo spazio di lavoro",
@ -674,19 +664,11 @@ const TRANSLATIONS = {
"Per favore, fornisci il testo originale che desideri che venga riformulato.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda è alla ricerca di un nuovo dipendente per ricoprire la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda è alla ricerca di un nuovo dipendente per la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\nuser>Regenerate response\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.",
good_response: "Ottima risposta.",
more_actions: "Ulteriori azioni",
hide_citations: "Nascondi le citazioni",
show_citations: "Mostra citazioni",
fork: "Forchetta",
delete: "Elimina",
save_submit: "Salva e invia",
cancel: "Annulla",
edit_prompt: "Suggerimento di modifica:",
edit_response: "Modifica la risposta",
at_agent: "@agent",
default_agent_description:
"- l'agente predefinito per questo spazio di lavoro.",
custom_agents_coming_soon: "Agenti personalizzati in arrivo a breve!",
slash_reset: "/reset",
preset_reset_description:
"Elimina la cronologia delle chat e avvia una nuova chat",
add_new_preset: "Aggiungi nuovo preset",
@ -716,6 +698,37 @@ const TRANSLATIONS = {
missing_credentials_description:
"Fare clic per configurare le credenziali",
},
submit: "Invia",
edit_info_user:
'"Invia" rigenera la risposta dell\'IA. "Salva" aggiorna solo il tuo messaggio.',
edit_info_assistant:
"Le modifiche verranno salvate direttamente in questa risposta.",
see_less: "Visualizza meno",
see_more: "Visualizza altro",
tools: "Strumenti",
browse: "Naviga",
text_size_label: "Dimensione del testo",
select_model: "Seleziona il modello",
sources: "Fonti",
document: "Documento",
similarity_match: "partita",
source_count_one: "Riferimento {{count}}",
source_count_other: "Riferimenti a {{count}}",
preset_exit_description: "Interrompere la sessione corrente con l'agente.",
add_new: "Aggiungi nuovo",
edit: "Modifica",
publish: "Pubblicare",
stop_generating: "Interrompi la generazione della risposta",
pause_tts_speech_message:
"Mettere in pausa la lettura vocale del messaggio",
slash_commands: "Comandi abbreviati",
agent_skills: "Competenze dell'agente",
manage_agent_skills: "Gestire le competenze degli agenti",
agent_skills_disabled_in_session:
"Non è possibile modificare le competenze durante una sessione di agente attivo. Per terminare la sessione, utilizzare il comando `/exit`.",
start_agent_session: "Avvia sessione agente",
use_agent_session_to_use_tools:
'È possibile utilizzare gli strumenti disponibili tramite chat avviando una sessione con un agente utilizzando il prefisso "@agent" all\'inizio del messaggio.',
},
profile_settings: {
edit_account: "Modifica account",
@ -788,11 +801,6 @@ const TRANSLATIONS = {
description:
"Definisci un nome che verrà visualizzato sulla pagina di accesso per tutti gli utenti.",
},
"chat-message-alignment": {
title: "Allignment di conversazioni",
description:
"Seleziona la modalità di allineamento del messaggio quando utilizzi l'interfaccia di chat.",
},
"display-language": {
title: "Lingua da visualizzare",
description:

View File

@ -148,13 +148,6 @@ const TRANSLATIONS = {
heading: "説明してください",
body: "AnythingLLMの利点",
},
pfp: {
title: "アシスタントのプロフィール画像",
description:
"このワークスペースのアシスタントのプロフィール画像をカスタマイズします。",
image: "ワークスペース画像",
remove: "ワークスペース画像を削除",
},
delete: {
title: "ワークスペースを削除",
description:
@ -646,8 +639,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "メッセージを送信",
attach_file: "このチャットにファイルを添付",
slash: "チャットで使えるスラッシュコマンドをすべて表示",
agents: "利用可能なエージェントをすべて表示",
text_size: "テキストサイズを変更",
microphone: "プロンプトを音声入力",
send: "ワークスペースにプロンプトメッセージを送信",
@ -660,18 +651,11 @@ const TRANSLATIONS = {
good_response: "良い反応",
more_actions:
"さらに詳細な情報が必要な場合は、お気軽にお問い合わせください。",
hide_citations: "参考文献を隠す",
show_citations: "引用元を表示する",
fork: "フォーク",
delete: "削除",
save_submit: "保存して送信",
cancel: "キャンセル",
edit_prompt: "編集のヒント",
edit_response: "編集内容を保存します。",
at_agent: "@agent",
default_agent_description: "- このワークスペースのデフォルトエージェント。",
custom_agents_coming_soon: "カスタムエージェントは近日公開予定です。",
slash_reset: "/reset",
preset_reset_description:
"チャット履歴をクリアし、新しいチャットを開始してください。",
add_new_preset: "新しいプリセットを追加する",
@ -696,6 +680,35 @@ const TRANSLATIONS = {
missing_credentials_description:
"認証情報を設定するには、ここをクリックしてください。",
},
submit: "送信",
edit_info_user:
"「送信」はAIの応答を再生成します。「保存」は、あなたのメッセージのみを更新します。",
edit_info_assistant: "あなたの変更は、この回答に直接保存されます。",
see_less: "詳細を見る",
see_more: "詳細を見る",
tools: "道具",
browse: "閲覧",
text_size_label: "文字サイズ",
select_model: "モデルを選択",
sources: "出典",
document: "文書",
similarity_match: "試合",
source_count_one: "{{count}} 参照",
source_count_other: "{{count}} への参照",
preset_exit_description: "現在のエージェントセッションを停止する",
add_new: "新しいものを追加する",
edit: "編集",
publish: "出版",
stop_generating: "応答の生成を停止する",
pause_tts_speech_message: "メッセージのテキスト読み上げ機能を一時停止する",
slash_commands: "スラッシュコマンド",
agent_skills: "エージェントのスキル",
manage_agent_skills: "エージェントのスキル管理",
agent_skills_disabled_in_session:
"アクティブなセッション中にスキルを変更することはできません。まず、`/exit`コマンドを使用してセッションを終了してください。",
start_agent_session: "エージェントセッションを開始",
use_agent_session_to_use_tools:
"チャットでツールを使用するには、プロンプトの冒頭に'@agent'を使用してエージェントセッションを開始してください。",
},
profile_settings: {
edit_account: "アカウントを編集",
@ -764,11 +777,6 @@ const TRANSLATIONS = {
description:
"ログインページに表示される名前を、すべてのユーザーに設定する。",
},
"chat-message-alignment": {
title: "チャットメッセージの整合性を確認する",
description:
"チャットインターフェースを使用する場合、メッセージの配置モードを選択してください。",
},
"display-language": {
title: "表示言語",
description:

View File

@ -153,12 +153,6 @@ const TRANSLATIONS = {
heading: "저에게 설명해주세요",
body: "AnythingLLM의 장점",
},
pfp: {
title: "어시스턴트 프로필 이미지",
description: "이 워크스페이스의 어시스턴트 프로필 이미지를 수정합니다.",
image: "워크스페이스 이미지",
remove: "워크스페이스 이미지 제거",
},
delete: {
title: "워크스페이스 삭제",
description:
@ -383,10 +377,6 @@ const TRANSLATIONS = {
description:
"로그인 페이지에 모든 사용자에게 표시될 애플리케이션 이름을 설정하세요.",
},
"chat-message-alignment": {
title: "채팅 메시지 정렬",
description: "채팅 인터페이스에서 메시지 정렬 방식을 선택하세요.",
},
"display-language": {
title: "표시 언어",
description:
@ -751,8 +741,6 @@ const TRANSLATIONS = {
"첨부 파일을 처리 중입니다. 잠시만 기다려 주세요...",
send_message: "메시지 보내기",
attach_file: "이 채팅에 파일 첨부",
slash: "채팅에서 사용할 수 있는 모든 슬래시 명령어 보기",
agents: "채팅에 사용할 수 있는 모든 에이전트 보기",
text_size: "텍스트 크기 변경",
microphone: "프롬프트를 음성으로 입력",
send: "프롬프트 메시지를 워크스페이스로 전송",
@ -762,18 +750,11 @@ const TRANSLATIONS = {
regenerate_response: "응답 다시 생성",
good_response: "좋은 답변",
more_actions: "더 많은 작업",
hide_citations: "인용 숨기기",
show_citations: "인용 보기",
fork: "포크",
delete: "삭제",
save_submit: "저장 및 제출",
cancel: "취소",
edit_prompt: "프롬프트 수정",
edit_response: "응답 수정",
at_agent: "@agent",
default_agent_description: " - 이 워크스페이스의 기본 에이전트입니다.",
custom_agents_coming_soon: "커스텀 에이전트 기능이 곧 제공됩니다!",
slash_reset: "/reset",
preset_reset_description: "채팅 기록을 초기화하고 새 채팅을 시작합니다",
add_new_preset: "새 프리셋 추가",
command: "명령어",
@ -796,6 +777,35 @@ const TRANSLATIONS = {
missing_credentials: "이 제공자의 인증 정보가 없습니다!",
missing_credentials_description: "클릭하여 인증 정보를 설정하세요",
},
submit: "제출",
edit_info_user:
'"제출"은 AI 응답을 다시 생성합니다. "저장"은 사용자 메시지만 업데이트합니다.',
edit_info_assistant: "당신이 변경한 내용은 바로 이 답변에 저장됩니다.",
see_less: "더 보기",
see_more: "더 보기",
tools: "도구",
browse: "검색",
text_size_label: "글자 크기",
select_model: "모델 선택",
sources: "출처",
document: "문서",
similarity_match: "경쟁",
source_count_one: "{{count}} 참조",
source_count_other: "{{count}} 관련 참고 자료",
preset_exit_description: "현재 에이전트 세션을 중단",
add_new: "새로운 항목 추가",
edit: "수정",
publish: "출판",
stop_generating: "응답 생성 중단",
pause_tts_speech_message: "메시지의 텍스트 음성 변환(TTS) 기능을 일시 중지",
slash_commands: "슬래시 명령어",
agent_skills: "에이전트의 역량",
manage_agent_skills: "에이전트 역량 관리",
agent_skills_disabled_in_session:
"활성 에이전트 세션 중에 기술을 변경할 수 없습니다. 먼저 /exit 명령을 사용하여 세션을 종료하십시오.",
start_agent_session: "에이전트 세션 시작",
use_agent_session_to_use_tools:
"채팅에서 도구를 사용하려면, 프롬프트의 시작 부분에 '@agent'을 사용하여 에이전트 세션을 시작할 수 있습니다.",
},
profile_settings: {
edit_account: "계정 정보 수정",

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Izskaidro man",
body: "AnythingLLM priekšrocības",
},
pfp: {
title: "Asistenta profila attēls",
description: "Pielāgojiet asistenta profila attēlu šai darba telpai.",
image: "Darba telpas attēls",
remove: "Noņemt darba telpas attēlu",
},
delete: {
title: "Dzēst darba telpu",
description:
@ -388,11 +382,6 @@ const TRANSLATIONS = {
description:
"Iestatiet nosaukumu, kas tiek rādīts pieteikšanās lapā visiem lietotājiem.",
},
"chat-message-alignment": {
title: "Sarunas ziņu līdzinājums",
description:
"Izvēlieties ziņu līdzinājuma režīmu, izmantojot sarunas saskarni.",
},
"display-language": {
title: "Displeja valoda",
description:
@ -765,8 +754,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Sūtīt ziņojumu",
attach_file: "Pievienot failu šim čatam",
slash: "Skatīt visas pieejamās slīpsvītras komandas čatošanai.",
agents: "Skatīt visus pieejamos aģentus, kurus varat izmantot čatošanai.",
text_size: "Mainīt teksta izmēru.",
microphone: "Izrunājiet savu uzvedni.",
send: "Nosūtīt uzvednes ziņojumu uz darba vietu",
@ -777,19 +764,11 @@ const TRANSLATIONS = {
regenerate_response: "Atjaunot atbildi",
good_response: "Laba atbilde",
more_actions: "Vairāk darbību",
hide_citations: "Izvākt atsaukmes",
show_citations: "Rādīt atsauces",
fork: "Klūtis",
delete: "Dzēst",
save_submit: "Saglabāt un iesūt",
cancel: "Atcelt",
edit_prompt: "Ieslēgt",
edit_response: "Rediģēt atbildi",
at_agent: "@agent",
default_agent_description: "- noklusējuma aģents šim darba telpai.",
custom_agents_coming_soon:
"Nedaudz drīzumā būs pieejami individuāli pakalpojumi!",
slash_reset: "/reset",
preset_reset_description:
"Izdzēsiet savu pastā veidoتو sarunu vēsturi un sāciet jaunu sarunu.",
add_new_preset: "Pievienot jaunu iepriekšējo",
@ -816,6 +795,37 @@ const TRANSLATIONS = {
missing_credentials_description:
"Noklikšķiniet, lai konfigurētu autentifikācijas datus",
},
submit: "Iesniegt",
edit_info_user:
'"Sūtīt" atjauno AI atbildi. "Saglabāt" atjauno tikai jūsu ziņu.',
edit_info_assistant:
"Jūsu izmaiņas tiks automātiski saglabātas šajā atbildē.",
see_less: "Skatīt mazāk",
see_more: "Skatīt vairāk",
tools: "Rīki",
browse: "Izpētiet",
text_size_label: "Teksta izmērs",
select_model: "Izvēlieties modeli",
sources: "Avotus",
document: "Dokuments",
similarity_match: "spēle",
source_count_one: "{{count}} atsauce",
source_count_other: "Atsauces uz {{count}}",
preset_exit_description: "Aizust klientu sesiju",
add_new: "Pievienot jaunu",
edit: "Rediģēt",
publish: "Publicēt",
stop_generating: "Atsauciet atbildes ģenerēšanu",
pause_tts_speech_message:
"Pārtrauciet TTS (teksta-izrunas) žēstā vēstījuma izrunu.",
slash_commands: "Īs termini komandās",
agent_skills: "Aģenta prasmes",
manage_agent_skills: "Iesaista aģenta prasmes",
agent_skills_disabled_in_session:
"Nav iespējams mainīt prasmes aktīvā lietotāja sesijā. Pirmais, jāizmanto komandu `/exit`, lai beigtu sesiju.",
start_agent_session: "Sākt aģenta sesiju",
use_agent_session_to_use_tools:
'Jūs varat izmantot rīkus čatā, sākot aģenta sesiju, ievietojot "@agent" jūsu iniciālajā tekstā.',
},
profile_settings: {
edit_account: "Rediģēt kontu",

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Leg me uit",
body: "de voordelen van AnythingLLM",
},
pfp: {
title: "Assistent Profielfoto",
description:
"Pas de profielfoto van de assistent voor deze werkruimte aan.",
image: "Werkruimte Afbeelding",
remove: "Werkruimte Afbeelding Verwijderen",
},
delete: {
title: "Werkruimte Verwijderen",
description:
@ -656,9 +649,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Een bericht verzenden",
attach_file: "Een bestand aan deze chat toevoegen",
slash: "Alle beschikbare slash-opdrachten voor chatten bekijken.",
agents:
"Alle beschikbare agents bekijken die je kunt gebruiken om te chatten.",
text_size: "Tekstgrootte wijzigen.",
microphone: "Spreek je prompt uit.",
send: "Promptbericht naar werkruimte verzenden",
@ -670,18 +660,11 @@ const TRANSLATIONS = {
regenerate_response: "Reactie opnieuw genereren",
good_response: "Goede reactie",
more_actions: "Meer acties",
hide_citations: "Citaten verbergen",
show_citations: "Citaten weergeven",
fork: "Fork",
delete: "Verwijderen",
save_submit: "Opslaan en verzenden",
cancel: "Annuleren",
edit_prompt: "Prompt bewerken",
edit_response: "Reactie bewerken",
at_agent: "@agent",
default_agent_description: " - de standaardagent voor deze werkruimte.",
custom_agents_coming_soon: "Aangepaste agenten komen binnenkort!",
slash_reset: "/reset",
preset_reset_description:
"Wis je chatgeschiedenis en begin een nieuwe chat",
add_new_preset: "Nieuwe preset toevoegen",
@ -704,6 +687,36 @@ const TRANSLATIONS = {
missing_credentials: "Deze aanbieder mist logingegevens!",
missing_credentials_description: "Klik om logingegevens in te stellen",
},
submit: "Indienen",
edit_info_user:
'"Verzenden" herstelt het antwoord van de AI. "Opslaan" wijzigt alleen uw bericht.',
edit_info_assistant:
"Uw wijzigingen worden direct op deze reactie opgeslagen.",
see_less: "Minder zien",
see_more: "Meer zien",
tools: "Gereedschap",
browse: "Bladeren",
text_size_label: "Lettergrootte",
select_model: "Kies het model",
sources: "Bronnen",
document: "Document",
similarity_match: "wedstrijd",
source_count_one: "{{count}} verwijzing",
source_count_other: "{{count}} referenties",
preset_exit_description: "Beëindig de huidige agent-sessie",
add_new: "Voeg toe",
edit: "Bewerk",
publish: "Publiceren",
stop_generating: "Stoppen met het genereren van antwoorden",
pause_tts_speech_message: "Pauzeer de spraak van de tekstberichten.",
slash_commands: "Korte commando's",
agent_skills: "Vaardigheden van agenten",
manage_agent_skills: "Beheer van de vaardigheden van de agent",
agent_skills_disabled_in_session:
"Het is niet mogelijk om vaardigheden aan te passen tijdens een actieve sessie. Gebruik eerst de commando `/exit` om de sessie te beëindigen.",
start_agent_session: "Start Agent Sessie",
use_agent_session_to_use_tools:
'U kunt tools in de chat gebruiken door een sessie met een agent te starten, beginnend met "@agent" aan het begin van uw bericht.',
},
profile_settings: {
edit_account: "Account bewerken",
@ -772,11 +785,6 @@ const TRANSLATIONS = {
description:
"Stel een naam in die op de inlogpagina voor alle gebruikers wordt weergegeven.",
},
"chat-message-alignment": {
title: "Uitlijning van chatberichten",
description:
"Selecteer de uitlijningsmodus voor berichten bij gebruik van de chatinterface.",
},
"display-language": {
title: "Weergavetaal",
description:

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Wyjaśnij mi",
body: "Korzyści z AnythingLLM",
},
pfp: {
title: "Logo obszaru roboczego",
description: "Dostosuj logo asystenta dla tego obszaru roboczego.",
image: "Logo obszaru roboczego",
remove: "Usuń logo obszaru roboczego",
},
delete: {
title: "Usuń obszar roboczy",
description:
@ -390,11 +384,6 @@ const TRANSLATIONS = {
description:
"Ustawienie nazwy wyświetlanej na stronie logowania dla wszystkich użytkowników.",
},
"chat-message-alignment": {
title: "Wyrównanie wiadomości czatu",
description:
"Wybór trybu wyrównania wiadomości podczas korzystania z interfejsu czatu.",
},
"display-language": {
title: "Język",
description:
@ -767,8 +756,6 @@ const TRANSLATIONS = {
attachments_processing: "Załączniki są przetwarzane. Proszę czekać...",
send_message: "Wyślij wiadomość",
attach_file: "Dołącz plik do tego czatu",
slash: "Wyświetl wszystkie dostępne polecenia slash do czatowania.",
agents: "Wyświetl wszystkich dostępnych agentów.",
text_size: "Zmiana rozmiaru tekstu.",
microphone: "Wypowiedz swoją prośbę.",
send: "Wyślij wiadomość do obszaru roboczego",
@ -778,18 +765,11 @@ const TRANSLATIONS = {
regenerate_response: "Wygeneruj ponownie odpowiedź",
good_response: "Dobra odpowiedź",
more_actions: "Więcej działań",
hide_citations: "Ukryj cytaty",
show_citations: "Pokaż cytaty",
fork: "Utwórz rozgałęzienie",
delete: "Usuń",
save_submit: "Zapisz i prześlij",
cancel: "Anuluj",
edit_prompt: "Edytuj prompt",
edit_response: "Edytuj odpowiedź",
at_agent: "@agent",
default_agent_description: " - domyślny agent dla tego obszaru roboczego.",
custom_agents_coming_soon: "niestandardowi agenci już wkrótce!",
slash_reset: "/reset",
preset_reset_description: "Wyczyść historię czatu i rozpocznij nowy czat",
add_new_preset: " Dodaj nowe polecenie slash",
command: "Polecenie",
@ -813,6 +793,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Kliknij, aby skonfigurować poświadczenia",
},
submit: "Prześlij",
edit_info_user:
'"Wyślij" powoduje ponowne wygenerowanie odpowiedzi przez sztuczną inteligencję. "Zapisz" aktualizuje tylko Twoje wiadomości.',
edit_info_assistant:
"Twoje zmiany zostaną zapisane bezpośrednio w tej odpowiedzi.",
see_less: "Zobacz mniej",
see_more: "Zobacz więcej",
tools: "Narzędzia",
browse: "Przeglądaj",
text_size_label: "Rozmiar czcionki",
select_model: "Wybierz model",
sources: "Źródła",
document: "Dokument",
similarity_match: "mecz",
source_count_one: "{{count}} odniesienie",
source_count_other: "{{count}} odnośniki",
preset_exit_description: "Zakończ bieżącą sesję z przedstawicielem",
add_new: "Dodaj nowe",
edit: "Edytuj",
publish: "Opublikować",
stop_generating: "Przestań generować odpowiedź",
pause_tts_speech_message: "Wstrzymać odtwarzanie mowy z wiadomości",
slash_commands: "Polecenia skrótowe",
agent_skills: "Umiejętności agenta",
manage_agent_skills: "Zarządzanie umiejętnościami agentów",
agent_skills_disabled_in_session:
"Nie można modyfikować umiejętności podczas trwającej sesji. Aby zakończyć sesję, należy najpierw użyć komendy /exit.",
start_agent_session: "Rozpocznij sesję dla agenta",
use_agent_session_to_use_tools:
"Możesz korzystać z narzędzi w czacie, inicjując sesję z agentem, wpisując '@agent' na początku swojego zapytania.",
},
profile_settings: {
edit_account: "Edytuj konto",

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Explique para mim",
body: "os benefícios do AnythingLLM",
},
pfp: {
title: "Imagem do Assistente",
description: "Personalize a imagem do assistente para este workspace.",
image: "Imagem do Workspace",
remove: "Remover Imagem",
},
delete: {
title: "Excluir Workspace",
description:
@ -384,10 +378,6 @@ const TRANSLATIONS = {
description:
"Defina um nome exibido na página de login para todos os usuários.",
},
"chat-message-alignment": {
title: "Alinhamento de Mensagens",
description: "Selecione o alinhamento das mensagens no chat.",
},
"display-language": {
title: "Idioma",
description:
@ -748,8 +738,6 @@ const TRANSLATIONS = {
attachments_processing: "Anexos em processamento. Aguarde...",
send_message: "Enviar mensagem",
attach_file: "Anexar arquivo ao chat",
slash: "Veja todos os comandos disponíveis.",
agents: "Veja todos os agentes disponíveis.",
text_size: "Alterar tamanho do texto.",
microphone: "Fale seu prompt.",
send: "Enviar prompt para o workspace",
@ -759,18 +747,11 @@ const TRANSLATIONS = {
regenerate_response: "Regerar resposta",
good_response: "Resposta satisfatória",
more_actions: "Mais ações",
hide_citations: "Esconder citações",
show_citations: "Exibir citações",
fork: "Fork",
delete: "Excluir",
save_submit: "Alterar",
cancel: "Cancelar",
edit_prompt: "Editar prompt",
edit_response: "Editar resposta",
at_agent: "@agent",
default_agent_description: " - o agente padrão deste workspace.",
custom_agents_coming_soon: "mais agentes personalizados em breve!",
slash_reset: "/reset",
preset_reset_description: "Limpa o histórico do seu chat e inicia um novo",
add_new_preset: " Insere um novo Preset",
command: "Comando",
@ -794,6 +775,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Configure as credenciais do LLM primeiro",
},
submit: "Enviar",
edit_info_user:
'"Enviar" recria a resposta da IA. "Salvar" atualiza apenas sua mensagem.',
edit_info_assistant:
"Suas alterações serão salvas diretamente nesta resposta.",
see_less: "Ver menos",
see_more: "Ver mais",
tools: "Ferramentas",
browse: "Navegar",
text_size_label: "Tamanho do texto",
select_model: "Selecione o modelo",
sources: "Fontes",
document: "Documento",
similarity_match: "jogo",
source_count_one: "Referência a {{count}}",
source_count_other: "Referências a {{count}}",
preset_exit_description: "Interrompa a sessão atual do agente",
add_new: "Adicionar novo",
edit: "Editar",
publish: "Publicar",
stop_generating: "Pare de gerar respostas",
pause_tts_speech_message: "Pausar a leitura de voz da mensagem",
slash_commands: "Comandos Rápidos",
agent_skills: "Habilidades do Agente",
manage_agent_skills: "Gerenciar as habilidades dos agentes",
agent_skills_disabled_in_session:
"Não é possível modificar as habilidades durante uma sessão de agente ativa. Utilize o comando `/exit` para encerrar a sessão primeiro.",
start_agent_session: "Iniciar Sessão de Agente",
use_agent_session_to_use_tools:
'Você pode utilizar as ferramentas disponíveis no chat iniciando uma sessão com um agente, adicionando "@agent" no início da sua mensagem.',
},
profile_settings: {
edit_account: "Editar conta",

View File

@ -158,13 +158,6 @@ const TRANSLATIONS = {
heading: "Explică-mi",
body: "beneficiile AnythingLLM",
},
pfp: {
title: "Imagine profil asistent",
description:
"Personalizează imaginea de profil a asistentului pentru acest spațiu de lucru.",
image: "Imagine spațiu de lucru",
remove: "Șterge imaginea spațiului de lucru",
},
delete: {
title: "Șterge spațiul de lucru",
description:
@ -498,8 +491,6 @@ const TRANSLATIONS = {
"Fișierele atașate se procesează. Te rugăm să aștepți...",
send_message: "Trimite mesaj",
attach_file: "Atașează un fișier la acest chat",
slash: "Vizualizează toate comenzile slash disponibile pentru chat.",
agents: "Vezi toți agenții disponibili pentru chat.",
text_size: "Schimbă dimensiunea textului.",
microphone: "Vorbește promptul tău.",
send: "Trimite prompt către spațiul de lucru",
@ -509,19 +500,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerare răspuns",
good_response: "Răspuns bun",
more_actions: "Mai multe acțiuni",
hide_citations: "Ascunde citările",
show_citations: "Arată citările",
fork: "Fork",
delete: "Șterge",
save_submit: "Salvează & Trimite",
cancel: "Anulează",
edit_prompt: "Editează prompt",
edit_response: "Editează răspuns",
at_agent: "@agent",
default_agent_description:
" - agentul implicit pentru acest spațiu de lucru.",
custom_agents_coming_soon: "agenții personalizați vin în curând!",
slash_reset: "/reset",
preset_reset_description:
"Șterge istoricul chatului și începe o conversație nouă",
add_new_preset: " Adaugă preset nou",
@ -546,6 +529,37 @@ const TRANSLATIONS = {
missing_credentials: "Acest furnizor lipsește credențiale!",
missing_credentials_description: "Click pentru a configura credențialele",
},
submit: "Trimite",
edit_info_user:
"„Trimite” recreează răspunsul generat de inteligența artificială. „Salvează” actualizează doar mesajul dumneavoastră.",
edit_info_assistant:
"Modificările pe care le faceți vor fi salvate direct în acest răspuns.",
see_less: "Vezi mai puțin",
see_more: "Vezi mai multe",
tools: "Unelte",
browse: "Navigați",
text_size_label: "Dimensiunea textului",
select_model: "Selectați modelul",
sources: "Surse",
document: "Document",
similarity_match: "meci",
source_count_one: "{{count}} referință",
source_count_other: "Referințe către {{count}}",
preset_exit_description: "Întrerupeți sesiunea actuală a agentului",
add_new: "Adaugă",
edit: "Editează",
publish: "Publica",
stop_generating: "Opriți generarea răspunsului",
pause_tts_speech_message:
"Pauză în redarea vocii prin Text-to-Speech (TTS) a mesajului.",
slash_commands: "Comenzi scurte",
agent_skills: "Abilități ale agentului",
manage_agent_skills: "Gestionarea competențelor agenților",
agent_skills_disabled_in_session:
"Nu este posibil să modificați abilitățile în timpul unei sesiuni cu un agent activ. Pentru a încheia sesiunea, utilizați comanda /exit.",
start_agent_session: "Începe sesiunea de agent",
use_agent_session_to_use_tools:
'Puteți utiliza instrumentele disponibile în chat, inițiind o sesiune cu un agent, începând mesajul cu "@agent".',
},
profile_settings: {
edit_account: "Editează contul",
@ -815,11 +829,6 @@ const TRANSLATIONS = {
description:
"Setează un nume care este afișat pe pagina de autentificare tuturor utilizatorilor.",
},
"chat-message-alignment": {
title: "Alinierea mesajelor de chat",
description:
"Selectează modul de aliniere a mesajelor când folosești interfața de chat.",
},
"display-language": {
title: "Limba de afișare",
description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Объясните мне",
body: "преимущества AnythingLLM",
},
pfp: {
title: "Изображение профиля помощника",
description:
"Настройте изображение профиля помощника для этого рабочего пространства.",
image: "Изображение рабочего пространства",
remove: "Удалить изображение рабочего пространства",
},
delete: {
title: "Удалить Рабочее Пространство",
description:
@ -654,8 +647,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Отправить сообщение",
attach_file: "Прикрепить файл к чату",
slash: "Просмотреть все доступные слэш-команды для чата.",
agents: "Просмотреть всех доступных агентов для чата.",
text_size: "Изменить размер текста.",
microphone: "Произнесите ваш запрос.",
send: "Отправить запрос в рабочее пространство",
@ -666,20 +657,12 @@ const TRANSLATIONS = {
regenerate_response: "Перефразировать ответ",
good_response: "Хороший ответ",
more_actions: "Больше действий",
hide_citations: "Скрыть ссылки на источники",
show_citations: "Отображение ссылок",
fork: "Вилка",
delete: "Удалить",
save_submit: "Сохранить и отправить",
cancel: "Отменить",
edit_prompt:
"Пожалуйста, предоставьте текст, который необходимо отредактировать.",
edit_response: "Отредактируйте ответ",
at_agent: "@agent",
default_agent_description:
"- это основной агент для данного рабочего пространства.",
custom_agents_coming_soon: "Скоро появятся индивидуальные агенты!",
slash_reset: "/reset",
preset_reset_description: "Очистите историю чата и начните новый чат",
add_new_preset: "Добавить новый шаблон",
command: "Команда",
@ -707,6 +690,37 @@ const TRANSLATIONS = {
missing_credentials_description:
"Нажмите, чтобы настроить учетные данные",
},
submit: "Отправить",
edit_info_user:
'"Отправить" генерирует новый ответ от ИИ. "Сохранить" обновляет только ваше сообщение.',
edit_info_assistant:
"Ваши изменения будут сохранены непосредственно в этом ответе.",
see_less: "Показать меньше",
see_more: "Узнать больше",
tools: "Инструменты",
browse: "Просматривать",
text_size_label: "Размер текста",
select_model: "Выберите модель",
sources: "Источники",
document: "Документ",
similarity_match: "соревнование; игра",
source_count_one: "{{count}} ссылка",
source_count_other: "Ссылки на {{count}}",
preset_exit_description: "Прекратить текущую сессию работы с агентом",
add_new: "Добавить новое",
edit: "Редактировать",
publish: "Опубликовать",
stop_generating: "Прекратите генерацию ответа",
pause_tts_speech_message:
"Приостановить чтение текста с помощью синтезатора речи.",
slash_commands: "Команды, введенные сокращенной формой",
agent_skills: "Навыки агента",
manage_agent_skills: "Управление навыками агентов",
agent_skills_disabled_in_session:
"Невозможно изменять навыки во время активной сессии. Для завершения сессии сначала используйте команду /exit.",
start_agent_session: "Начать сеанс для агента",
use_agent_session_to_use_tools:
"Вы можете использовать инструменты в чате, начав сеанс с агентом, добавив '@agent' в начало вашего сообщения.",
},
profile_settings: {
edit_account: "Редактировать учётную запись",
@ -777,11 +791,6 @@ const TRANSLATIONS = {
description:
"Укажите имя, которое будет отображаться на странице входа для всех пользователей.",
},
"chat-message-alignment": {
title: "Выравнивание сообщений в чате",
description:
"Выберите режим выравнивания сообщений при использовании интерфейса чата.",
},
"display-language": {
title: "Язык отображения",
description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Bana açıkla",
body: "AnythingLLM'nin faydalarını",
},
pfp: {
title: "Asistan Profil Görseli",
description:
"Bu çalışma alanı için asistanın profil resmini özelleştirin.",
image: "Çalışma Alanı Görseli",
remove: "Çalışma Alanı Görselini Kaldır",
},
delete: {
title: "Çalışma Alanını Sil",
description:
@ -653,8 +646,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Mesaj gönderin",
attach_file: "Bu sohbete bir dosya ekleyin",
slash: "Sohbet için mevcut tüm eğik çizgi komutlarını görüntüleyin.",
agents: "Sohbet için kullanabileceğiniz tüm ajanları görüntüleyin.",
text_size: "Metin boyutunu değiştirin.",
microphone: "Promptunuzu söyleyin.",
send: "Çalışma alanına prompt mesajı gönderin",
@ -665,18 +656,11 @@ const TRANSLATIONS = {
regenerate_response: "Yanıtı yeniden oluştur",
good_response: "İyi yanıt",
more_actions: "Daha fazla eylem",
hide_citations: "Alıntıları gizle",
show_citations: "Alıntıları göster",
fork: "Çatalla",
delete: "Sil",
save_submit: "Kaydet & Gönder",
cancel: "İptal",
edit_prompt: "Promptu düzenle",
edit_response: "Yanıtı düzenle",
at_agent: "@agent",
default_agent_description: " - bu çalışma alanının varsayılan ajanı.",
custom_agents_coming_soon: "özel ajanlar yakında!",
slash_reset: "/reset",
preset_reset_description:
"Sohbet geçmişinizi temizleyin ve yeni bir sohbet başlatın",
add_new_preset: " Yeni Ön Ayar Ekle",
@ -701,6 +685,36 @@ const TRANSLATIONS = {
missing_credentials_description:
"Kimlik bilgilerini ayarlamak için tıklayın",
},
submit: "Gönder",
edit_info_user:
'"Gönder" seçeneği, yapay zeka yanıtını yeniden oluşturur. "Kaydet" seçeneği, yalnızca sizin mesajınızı günceller.',
edit_info_assistant:
"Yaptığınız değişiklikler doğrudan bu yanıtın içine kaydedilecektir.",
see_less: "Daha az",
see_more: "Daha Fazla",
tools: "Araçlar",
browse: "Gezin",
text_size_label: "Metin Boyutu",
select_model: "Model Seçimi",
sources: "Kaynaklar",
document: "Belge",
similarity_match: "maç",
source_count_one: "{{count}} ile ilgili bilgi",
source_count_other: "{{count}} referansları",
preset_exit_description: "Mevcut ajan oturumunu durdurun",
add_new: "Yeni ekle",
edit: "Düzenle",
publish: "Yayınla",
stop_generating: "Yanıt üretmeyi durdurun",
pause_tts_speech_message: "Mesajın metin okuma (TTS) özelliğini durdur",
slash_commands: "Komut Satırı Komutları",
agent_skills: "Ajansın Yetenekleri",
manage_agent_skills: "Temsilcinin becerilerini yönetin",
agent_skills_disabled_in_session:
"Aktif bir ajan oturumunda becerileri değiştirilemez. İlk olarak /exit komutunu kullanarak oturumu sonlandırın.",
start_agent_session: "Temsilci Oturumu Başlat",
use_agent_session_to_use_tools:
'Çatınızdaki araçları kullanmak için, isteminizin başında "@agent" ile bir ajan oturumu başlatabilirsiniz.',
},
profile_settings: {
edit_account: "Hesabı Düzenle",
@ -770,11 +784,6 @@ const TRANSLATIONS = {
description:
"Giriş sayfasında tüm kullanıcılara gösterilen bir ad ayarlayın.",
},
"chat-message-alignment": {
title: "Sohbet Mesajı Hizalaması",
description:
"Sohbet arayüzünü kullanırken mesaj hizalama modunu seçin.",
},
"display-language": {
title: "Görüntüleme Dili",
description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Giải thích cho tôi",
body: "các lợi ích của AnythingLLM",
},
pfp: {
title: "Hình đại diện trợ lý",
description:
"Tùy chỉnh hình ảnh hồ sơ của trợ lý cho không gian làm việc này.",
image: "Hình ảnh Không gian làm việc",
remove: "Xóa Hình ảnh Không gian làm việc",
},
delete: {
title: "Xóa không gian làm việc",
description:
@ -651,8 +644,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "Gửi tin nhắn",
attach_file: "Đính kèm tệp vào cuộc trò chuyện này",
slash: "Xem tất cả các lệnh gạch chéo có sẵn để trò chuyện.",
agents: "Xem tất cả các agent có sẵn bạn có thể sử dụng để trò chuyện.",
text_size: "Thay đổi kích thước văn bản.",
microphone: "Nói prompt của bạn.",
send: "Gửi tin nhắn prompt đến không gian làm việc",
@ -663,18 +654,11 @@ const TRANSLATIONS = {
regenerate_response: "Tạo lại phản hồi",
good_response: "Phản hồi tốt",
more_actions: "Thêm hành động",
hide_citations: "Ẩn trích dẫn",
show_citations: "Hiện trích dẫn",
fork: "Rẽ nhánh",
delete: "Xóa",
save_submit: "Lưu & Gửi",
cancel: "Hủy",
edit_prompt: "Chỉnh sửa prompt",
edit_response: "Chỉnh sửa phản hồi",
at_agent: "@agent",
default_agent_description: " - agent mặc định cho không gian làm việc này.",
custom_agents_coming_soon: "agent tùy chỉnh sắp ra mắt!",
slash_reset: "/reset",
preset_reset_description:
"Xóa lịch sử trò chuyện và bắt đầu cuộc trò chuyện mới",
add_new_preset: " Thêm Cài đặt sẵn Mới",
@ -698,6 +682,36 @@ const TRANSLATIONS = {
missing_credentials: "Nhà cung cấp này thiếu thông tin đăng nhập!",
missing_credentials_description: "Nhấp để thiết lập thông tin đăng nhập",
},
submit: "Gửi",
edit_info_user:
'"Gửi" sẽ tạo lại phản hồi của AI. "Lưu" chỉ cập nhật tin nhắn của bạn.',
edit_info_assistant:
"Các thay đổi của bạn sẽ được lưu trực tiếp vào phản hồi này.",
see_less: "Xem ít hơn",
see_more: "Xem thêm",
tools: "Dụng cụ",
browse: "Duyệt",
text_size_label: "Kích thước văn bản",
select_model: "Chọn mẫu",
sources: "Nguồn",
document: "Tài liệu",
similarity_match: "trận đấu",
source_count_one: "{{count}} tham khảo",
source_count_other: "{{count}} Tham khảo",
preset_exit_description: "Dừng lại phiên làm việc hiện tại",
add_new: "Thêm mới",
edit: "Chỉnh sửa",
publish: "Đăng tải",
stop_generating: "Dừng tạo ra phản hồi",
pause_tts_speech_message: "Tạm dừng phát giọng đọc của tin nhắn",
slash_commands: "Lệnh tắt/bật",
agent_skills: "Kỹ năng của đại lý",
manage_agent_skills: "Quản lý kỹ năng của đại lý",
agent_skills_disabled_in_session:
"Không thể thay đổi kỹ năng trong khi đang tham gia phiên làm việc. Trước tiên, hãy sử dụng lệnh /exit để kết thúc phiên làm việc.",
start_agent_session: "Bắt đầu phiên làm việc với đại lý",
use_agent_session_to_use_tools:
"Bạn có thể sử dụng các công cụ trong cuộc trò chuyện bằng cách bắt đầu một phiên với trợ lý bằng cách sử dụng '@agent' ở đầu yêu cầu của bạn.",
},
profile_settings: {
edit_account: "Chỉnh sửa Tài khoản",
@ -766,11 +780,6 @@ const TRANSLATIONS = {
description:
"Đặt tên được hiển thị trên trang đăng nhập cho tất cả người dùng.",
},
"chat-message-alignment": {
title: "Căn chỉnh Tin nhắn Trò chuyện",
description:
"Chọn chế độ căn chỉnh tin nhắn khi sử dụng giao diện trò chuyện.",
},
"display-language": {
title: "Ngôn ngữ Hiển thị",
description:

View File

@ -151,12 +151,6 @@ const TRANSLATIONS = {
heading: "向我解释",
body: "AnythingLLM 的好处",
},
pfp: {
title: "助理头像",
description: "为此工作区自定义助手的个人资料图像。",
image: "工作区图像",
remove: "移除工作区图像",
},
delete: {
title: "删除工作区",
description: "删除此工作区及其所有数据。这将删除所有用户的工作区。",
@ -368,10 +362,6 @@ const TRANSLATIONS = {
title: "名称",
description: "设置所有用户在登录页面看到的名称。",
},
"chat-message-alignment": {
title: "聊天消息对齐方式",
description: "选择在聊天界面中使用的消息对齐模式。",
},
"display-language": {
title: "显示语言",
description: "选择显示 AnythingLLM 界面所用的语言(若有翻译可用)。",
@ -713,8 +703,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "发送消息",
attach_file: "向此对话附加文件",
slash: "查看所有可用的聊天斜杠命令。",
agents: "查看所有可用的聊天助手。",
text_size: "更改文字大小。",
microphone: "语音输入你的提示。",
send: "将提示消息发送到工作区",
@ -725,18 +713,11 @@ const TRANSLATIONS = {
regenerate_response: "重新回应",
good_response: "反应良好",
more_actions: "更多操作",
hide_citations: "隐藏引文",
show_citations: "显示引文",
fork: "分叉",
delete: "删除",
save_submit: "提交保存",
cancel: "取消",
edit_prompt: "编辑问题",
edit_response: "编辑回应",
at_agent: "@agent",
default_agent_description: " - 此工作区的预设代理。",
custom_agents_coming_soon: "自定义代理功能即将推出!",
slash_reset: "/reset",
preset_reset_description: "清除聊天纪录并开始新的聊天",
add_new_preset: "新增预设",
command: "指令",
@ -758,6 +739,34 @@ const TRANSLATIONS = {
missing_credentials: "缺少凭证",
missing_credentials_description: "缺少凭证说明",
},
submit: "提交",
edit_info_user: "“提交”会重新生成 AI 的回复。 “保存”只会更新您的消息。",
edit_info_assistant: "您所做的修改将直接保存到此处。",
see_less: "查看更多",
see_more: "查看更多",
tools: "工具",
browse: "浏览",
text_size_label: "字体大小",
select_model: "选择型号",
sources: "来源",
document: "文件",
similarity_match: "比赛",
source_count_one: "{{count}} 参考",
source_count_other: "{{count}} 相关资料",
preset_exit_description: "停止当前的代理会话",
add_new: "添加新",
edit: "编辑",
publish: "出版",
stop_generating: "停止生成回复",
pause_tts_speech_message: "暂停消息的语音合成TTS功能",
slash_commands: "快捷命令",
agent_skills: "代理人技能",
manage_agent_skills: "管理代理人技能",
agent_skills_disabled_in_session:
"在活动会话期间,无法修改技能。首先使用 /exit 命令结束会话。",
start_agent_session: "开始代理会",
use_agent_session_to_use_tools:
"您可以通过在提示词的开头使用'@agent'来启动与代理的聊天,从而使用聊天工具。",
},
profile_settings: {
edit_account: "编辑帐户",

View File

@ -143,12 +143,6 @@ const TRANSLATIONS = {
heading: "請向我說明",
body: "AnythingLLM 的優點",
},
pfp: {
title: "助理個人檔案圖片",
description: "自訂此工作區助理的個人檔案圖片。",
image: "工作區圖片",
remove: "移除工作區圖片",
},
delete: {
title: "刪除工作區",
description: "刪除此工作區及其所有資料。這將會為所有使用者刪除該工作區。",
@ -613,8 +607,6 @@ const TRANSLATIONS = {
chat_window: {
send_message: "發送訊息",
attach_file: "附加檔案到此對話",
slash: "查看所有可用的斜線指令。",
agents: "查看所有可用的聊天代理。",
text_size: "變更文字大小。",
microphone: "語音輸入提示。",
send: "將提示訊息發送到工作區",
@ -625,18 +617,11 @@ const TRANSLATIONS = {
regenerate_response: "重新回應",
good_response: "反應良好",
more_actions: "更多操作",
hide_citations: "隱藏引文",
show_citations: "顯示引文",
fork: "分叉",
delete: "刪除",
save_submit: "提交保存",
cancel: "取消",
edit_prompt: "編輯問題",
edit_response: "編輯回應",
at_agent: "@agent",
default_agent_description: " - 此工作區的預設代理。",
custom_agents_coming_soon: "自訂代理功能即將推出!",
slash_reset: "/reset",
preset_reset_description: "清除聊天紀錄並開始新的聊天",
add_new_preset: "新增預設",
command: "指令",
@ -658,6 +643,34 @@ const TRANSLATIONS = {
missing_credentials: "缺少憑證",
missing_credentials_description: "缺少憑證說明",
},
submit: "提交",
edit_info_user: "「提交」會重新產生 AI 的回覆。 「儲存」僅會更新您的訊息。",
edit_info_assistant: "您的修改將直接儲存到此處。",
see_less: "查看更多",
see_more: "查看更多",
tools: "工具",
browse: "瀏覽",
text_size_label: "文字大小",
select_model: "選擇模型",
sources: "來源",
document: "文件",
similarity_match: "比賽",
source_count_one: "{{count}} 參考",
source_count_other: "{{count}} 的相關資料",
preset_exit_description: "暫停目前的工作階段",
add_new: "新增",
edit: "編輯",
publish: "發行",
stop_generating: "停止生成回應",
pause_tts_speech_message: "暫停語音合成的訊息",
slash_commands: "簡短指令",
agent_skills: "代理人技能",
manage_agent_skills: "管理代理人技能",
agent_skills_disabled_in_session:
"在執行代理時,無法修改技能。請先使用 /exit 命令結束本次執行。",
start_agent_session: "開始代理會談",
use_agent_session_to_use_tools:
"您可以使用聊天中的工具,只需在您的指令開頭加上'@agent',即可開始與代理的對話。",
},
profile_settings: {
edit_account: "編輯帳戶",
@ -721,10 +734,6 @@ const TRANSLATIONS = {
title: "應用名稱",
description: "設定所有使用者在登入頁面上看到的應用名稱。",
},
"chat-message-alignment": {
title: "聊天訊息對齊方式",
description: "選擇使用聊天介面時訊息的對齊模式。",
},
"display-language": {
title: "顯示語言",
description: "選擇 AnythingLLM 使用者介面的顯示語言(如有提供翻譯)。",

View File

@ -99,20 +99,16 @@ const Workspace = {
return this.threads._deleteEditedChats(slug, threadSlug, startingId);
return this._deleteEditedChats(slug, startingId);
},
updateChatResponse: async function (
updateChat: async function (
slug = "",
threadSlug = "",
chatId,
newText
newText,
role = "assistant"
) {
if (!!threadSlug)
return this.threads._updateChatResponse(
slug,
threadSlug,
chatId,
newText
);
return this._updateChatResponse(slug, chatId, newText);
return this.threads._updateChat(slug, threadSlug, chatId, newText, role);
return this._updateChat(slug, chatId, newText, role);
},
multiplexStream: async function ({
workspaceSlug,
@ -398,11 +394,11 @@ const Workspace = {
return { success: false, error: e.message };
});
},
_updateChatResponse: async function (slug = "", chatId, newText) {
_updateChat: async function (slug = "", chatId, newText, role = "assistant") {
return await fetch(`${API_BASE}/workspace/${slug}/update-chat`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ chatId, newText }),
body: JSON.stringify({ chatId, newText, role }),
})
.then((res) => {
if (res.ok) return true;

View File

@ -184,18 +184,19 @@ const WorkspaceThread = {
return false;
});
},
_updateChatResponse: async function (
_updateChat: async function (
workspaceSlug = "",
threadSlug = "",
chatId,
newText
newText,
role = "assistant"
) {
return await fetch(
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update-chat`,
{
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ chatId, newText }),
body: JSON.stringify({ chatId, newText, role }),
}
)
.then((res) => {

View File

@ -3,7 +3,6 @@ import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import LanguagePreference from "../components/LanguagePreference";
import ThemePreference from "../components/ThemePreference";
import { MessageDirection } from "../components/MessageDirection";
export default function InterfaceSettings() {
const { t } = useTranslation();
@ -28,7 +27,6 @@ export default function InterfaceSettings() {
</div>
<ThemePreference />
<LanguagePreference />
<MessageDirection />
</div>
</div>
</div>

View File

@ -1,69 +0,0 @@
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
export function MessageDirection() {
const { t } = useTranslation();
const { msgDirection, setMsgDirection } = useChatMessageAlignment();
return (
<div className="flex flex-col gap-y-0.5 my-4">
<p className="text-sm leading-6 font-semibold text-white">
{t("customization.items.chat-message-alignment.title")}
</p>
<p className="text-xs text-white/60">
{t("customization.items.chat-message-alignment.description")}
</p>
<div className="flex flex-row flex-wrap gap-x-4 pt-1 gap-y-4 md:gap-y-0">
<ItemDirection
active={msgDirection === "left"}
reverse={false}
msg="User and AI messages are aligned to the left (default)"
onSelect={() => {
setMsgDirection("left");
}}
/>
<ItemDirection
active={msgDirection === "left_right"}
reverse={true}
msg="User and AI messages are distributed left and right alternating each message"
onSelect={() => {
setMsgDirection("left_right");
}}
/>
</div>
<Tooltip
id="alignment-choice-item"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}
function ItemDirection({ active, reverse, onSelect, msg }) {
return (
<button
data-tooltip-id="alignment-choice-item"
data-tooltip-content={msg}
type="button"
className={`flex:1 p-4 bg-transparent hover:light:bg-gray-100 hover:bg-gray-700/20 rounded-xl border w-[250px] ${active ? "border-primary-button" : " border-theme-sidebar-border"}`}
onClick={onSelect}
>
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className={`flex items-center justify-end gap-2 ${reverse && index % 2 === 0 ? "flex-row-reverse" : ""}`}
>
<div
className={`w-4 h-4 rounded-full ${index % 2 === 0 ? "bg-primary-button" : "bg-white light:bg-black"} flex-shrink-0`}
/>
<div className="bg-gray-600 light:bg-gray-200 rounded-2xl px-4 py-2 h-[20px] w-full" />
</div>
))}
</div>
</button>
);
}

View File

@ -23,6 +23,8 @@ import { safeJsonParse } from "@/utils/request";
import QuickActions from "@/components/lib/QuickActions";
import SuggestedMessages from "@/components/lib/SuggestedMessages";
import useUser from "@/hooks/useUser";
import TextSizeMenu from "@/components/WorkspaceChat/ChatContainer/TextSizeMenu";
import WorkspaceModelPicker from "@/components/WorkspaceChat/ChatContainer/WorkspaceModelPicker";
import { ChatTooltips } from "@/components/WorkspaceChat/ChatContainer/ChatTooltips";
async function getTargetWorkspace() {
@ -129,7 +131,7 @@ export default function Home() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden"
/>
);
}
@ -242,6 +244,12 @@ function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
writeMode = "replace",
}) {
if (autoSubmit) {
if (writeMode === "append") {
const currentText =
document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
text = currentText + text;
}
if (!text.trim()) return;
submitMessage(text.trim());
return;
}
@ -269,9 +277,11 @@ function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border"
>
{isMobile && <SidebarMobileHeader />}
<TextSizeMenu />
<WorkspaceModelPicker workspaceSlug={workspace?.slug} />
<DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full items-center justify-center">
<div className="flex flex-col items-center w-full max-w-[750px]">
@ -312,7 +322,7 @@ function NoWorkspacesAssigned() {
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden"
>
<div className="flex flex-col h-full w-full items-center justify-center">
<p className="text-white/60 text-sm text-center whitespace-pre-line">

View File

@ -13,7 +13,7 @@ export default function Main() {
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
return (
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
{!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
<Home />
</div>

View File

@ -31,11 +31,9 @@ function ShowWorkspaceChat() {
if (!_workspace) return setLoading(false);
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
const pfpUrl = await Workspace.fetchPfp(slug);
setWorkspace({
..._workspace,
suggestedMessages,
pfpUrl,
});
setLoading(false);
localStorage.setItem(
@ -51,7 +49,7 @@ function ShowWorkspaceChat() {
return (
<>
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
{!isMobile && <Sidebar />}
<WorkspaceChatContainer loading={loading} workspace={workspace} />
</div>

View File

@ -1,97 +0,0 @@
import Workspace from "@/models/workspace";
import showToast from "@/utils/toast";
import { Plus } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function WorkspacePfp({ workspace, slug }) {
const [pfp, setPfp] = useState(null);
const { t } = useTranslation();
useEffect(() => {
async function fetchWorkspace() {
const pfpUrl = await Workspace.fetchPfp(slug);
setPfp(pfpUrl);
}
fetchWorkspace();
}, [slug]);
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return false;
const formData = new FormData();
formData.append("file", file);
const { success, error } = await Workspace.uploadPfp(
formData,
workspace.slug
);
if (!success) {
showToast(`Failed to upload profile picture: ${error}`, "error");
return;
}
const pfpUrl = await Workspace.fetchPfp(workspace.slug);
setPfp(pfpUrl);
showToast("Profile picture uploaded.", "success");
};
const handleRemovePfp = async () => {
const { success, error } = await Workspace.removePfp(workspace.slug);
if (!success) {
showToast(`Failed to remove profile picture: ${error}`, "error");
return;
}
setPfp(null);
};
return (
<div className="mt-6">
<div className="flex flex-col">
<label className="block input-label">{t("general.pfp.title")}</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t("general.pfp.description")}
</p>
</div>
<div className="flex flex-col md:flex-row items-center gap-8">
<div className="flex flex-col items-center">
<label className="w-36 h-36 flex flex-col items-center justify-center bg-theme-settings-input-bg transition-all duration-300 rounded-full mt-8 border-2 border-dashed border-white border-opacity-60 cursor-pointer hover:opacity-60">
<input
id="workspace-pfp-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
{pfp ? (
<img
src={pfp}
alt="User profile picture"
className="w-36 h-36 rounded-full object-cover bg-theme-bg-secondary"
/>
) : (
<div className="flex flex-col items-center justify-center p-3">
<Plus className="w-8 h-8 text-theme-text-secondary m-2" />
<span className="text-theme-text-secondary text-opacity-80 text-xs font-semibold">
{t("general.pfp.image")}
</span>
<span className="text-theme-text-secondary text-opacity-60 text-xs">
800 x 800
</span>
</div>
)}
</label>
{pfp && (
<button
type="button"
onClick={handleRemovePfp}
className="mt-3 text-theme-text-secondary text-opacity-60 text-sm font-medium hover:underline"
>
{t("general.pfp.remove")}
</button>
)}
</div>
</div>
</div>
);
}

View File

@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
import WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace";
import WorkspacePfp from "./WorkspacePfp";
import CTAButton from "@/components/lib/CTAButton";
export default function GeneralInfo({ slug }) {
@ -65,7 +64,6 @@ export default function GeneralInfo({ slug }) {
/>
</form>
<SuggestedChatMessages slug={workspace.slug} />
<WorkspacePfp workspace={workspace} slug={slug} />
<DeleteWorkspace workspace={workspace} />
</div>
);

View File

@ -76,7 +76,7 @@ function ShowWorkspaceChat() {
const TabContent = TABS[tab];
return (
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}

View File

@ -213,8 +213,18 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
});
}
let _agentSessionActive = false;
export function setAgentSessionActive(value) {
_agentSessionActive = value;
}
export function getAgentSessionActive() {
return _agentSessionActive;
}
export function useIsAgentSessionActive() {
const [activeSession, setActiveSession] = useState(false);
const [activeSession, setActiveSession] = useState(
() => !!getAgentSessionActive()
);
useEffect(() => {
function listenForAgentSession() {
if (!window) return;

View File

@ -158,10 +158,7 @@ export default function handleChat(
}
// Action Handling via special 'action' attribute on response.
if (action === "reset_chat") {
// Chat was reset, keep reset message and clear everything else.
setChatHistory([_chatHistory.pop()]);
}
if (action === "reset_chat") setChatHistory([]);
// If thread was updated automatically based on chat prompt
// then we can handle the updating of the thread here.

View File

@ -216,9 +216,9 @@ function workspaceThreadEndpoints(app) {
],
async (request, response) => {
try {
const { chatId, newText = null } = reqBody(request);
const { chatId, newText = null, role = "assistant" } = reqBody(request);
if (!newText || !String(newText).trim())
throw new Error("Cannot save empty response");
throw new Error("Cannot save empty edit");
const user = await userFromSession(request, response);
const workspace = response.locals.workspace;
@ -231,15 +231,20 @@ function workspaceThreadEndpoints(app) {
});
if (!existingChat) throw new Error("Invalid chat.");
const chatResponse = safeJsonParse(existingChat.response, null);
if (!chatResponse) throw new Error("Failed to parse chat response");
await WorkspaceChats._update(existingChat.id, {
response: JSON.stringify({
...chatResponse,
text: String(newText),
}),
});
if (role === "user") {
await WorkspaceChats._update(existingChat.id, {
prompt: String(newText),
});
} else {
const chatResponse = safeJsonParse(existingChat.response, null);
if (!chatResponse) throw new Error("Failed to parse chat response");
await WorkspaceChats._update(existingChat.id, {
response: JSON.stringify({
...chatResponse,
text: String(newText),
}),
});
}
response.sendStatus(200).end();
} catch (e) {

View File

@ -454,9 +454,9 @@ function workspaceEndpoints(app) {
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
async (request, response) => {
try {
const { chatId, newText = null } = reqBody(request);
const { chatId, newText = null, role = "assistant" } = reqBody(request);
if (!newText || !String(newText).trim())
throw new Error("Cannot save empty response");
throw new Error("Cannot save empty edit");
const user = await userFromSession(request, response);
const workspace = response.locals.workspace;
@ -468,15 +468,20 @@ function workspaceEndpoints(app) {
});
if (!existingChat) throw new Error("Invalid chat.");
const chatResponse = safeJsonParse(existingChat.response, null);
if (!chatResponse) throw new Error("Failed to parse chat response");
await WorkspaceChats._update(existingChat.id, {
response: JSON.stringify({
...chatResponse,
text: String(newText),
}),
});
if (role === "user") {
await WorkspaceChats._update(existingChat.id, {
prompt: String(newText),
});
} else {
const chatResponse = safeJsonParse(existingChat.response, null);
if (!chatResponse) throw new Error("Failed to parse chat response");
await WorkspaceChats._update(existingChat.id, {
response: JSON.stringify({
...chatResponse,
text: String(newText),
}),
});
}
response.sendStatus(200).end();
} catch (e) {

View File

@ -19,7 +19,7 @@ async function resetMemory(
return {
uuid: msgUUID,
type: "textResponse",
textResponse: "Workspace chat memory was reset!",
textResponse: "Chat memory was reset!",
sources: [],
close: true,
error: false,