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

View File

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

View File

@ -1,10 +1,8 @@
import { Fragment, useState } from "react"; import { Fragment } from "react";
import { decode as HTMLDecode } from "he"; import { decode as HTMLDecode } from "he";
import truncate from "truncate"; import truncate from "truncate";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { middleTruncate } from "@/utils/directories";
import { import {
CaretRight,
FileText, FileText,
Info, Info,
ArrowSquareOut, ArrowSquareOut,
@ -14,16 +12,41 @@ import {
LinkSimple, LinkSimple,
GitlabLogo, GitlabLogo,
} from "@phosphor-icons/react"; } 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 { toPercentString } from "@/utils/numbers";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import pluralize from "pluralize"; import { useSourcesSidebar } from "../../SourcesSidebar";
import useTextSize from "@/hooks/useTextSize";
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 = {}; const combined = {};
sources.forEach((source) => { sources.forEach((source) => {
const { id, title, text, chunkSource = "", score = null } = source; const { id, title, text, chunkSource = "", score = null } = source;
@ -42,106 +65,83 @@ function combineLikeSources(sources) {
} }
export default function Citations({ sources = [] }) { export default function Citations({ sources = [] }) {
const [open, setOpen] = useState(false); const {
const [selectedSource, setSelectedSource] = useState(null); sidebarOpen,
openSidebar,
closeSidebar,
sources: currentSources,
} = useSourcesSidebar();
const { t } = useTranslation(); const { t } = useTranslation();
const { textSizeClass } = useTextSize();
if (sources.length === 0) return null; if (sources.length === 0) return null;
return ( const combined = combineLikeSources(sources);
<div className="flex flex-col mt-4 justify-left"> const visibleSources = combined.slice(0, 3);
<button const remainingCount = Math.max(0, combined.length - 3);
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 Citation = ({ source, onClick, textSizeClass }) => { function handleOpenSourcesSidebar() {
const { title, references = 1 } = source; if (sidebarOpen && sources === currentSources) {
if (!title) return null; closeSidebar();
const chunkSourceInfo = parseChunkSource(source); } else {
const truncatedTitle = chunkSourceInfo?.text ?? middleTruncate(title, 25); openSidebar(sources);
const CitationIcon = ICONS.hasOwnProperty(chunkSourceInfo?.icon) }
? ICONS[chunkSourceInfo.icon] }
: ICONS.file;
return ( return (
<button <button
className={`flex doc__source gap-x-1 ${textSizeClass}`} onClick={handleOpenSourcesSidebar}
onClick={onClick} 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" type="button"
> >
<div className="flex items-start flex-1 pt-[4px]"> <span className="text-xs text-white light:text-slate-800">
<CitationIcon size={16} /> {t("chat_window.sources")}
</div> </span>
<div className="flex flex-col items-start gap-y-[0.2px] px-1"> <div
<p className="relative h-[22px]"
className={`!m-0 font-semibold whitespace-nowrap text-theme-text-primary hover:opacity-55 ${textSizeClass}`} style={{ width: `${visibleSources.length * 17 + 5}px` }}
> >
{truncatedTitle} {visibleSources.map((source, idx) => {
</p> const info = parseChunkSource(source);
<p return (
className={`!m-0 text-[10px] font-medium text-theme-text-secondary ${textSizeClass}`} <div
>{`${references} ${pluralize("Reference", Number(references) || 1)}`}</p> 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> </div>
{remainingCount > 0 && (
<span className="text-xs text-white light:text-slate-800">
+ {remainingCount}
</span>
)}
</button> </button>
); );
}; }
function omitChunkHeader(text) { export function omitChunkHeader(text) {
if (!text.includes("<document_metadata>")) return text; if (!text.includes("<document_metadata>")) return text;
return text.split("</document_metadata>")[1].trim(); return text.split("</document_metadata>")[1].trim();
} }
function CitationDetailModal({ source, onClose }) { export function CitationDetailModal({ source, onClose }) {
const { references, title, chunks } = source; const { references, title, chunks } = source;
const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source); const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);
const { t } = useTranslation();
return ( return (
<ModalWrapper isOpen={!!source}> <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="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-theme-modal-border"> <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"> <div className="w-full flex gap-x-2 items-center">
{isUrl ? ( {isUrl ? (
<a <a
href={linkTo} href={linkTo}
target="_blank" target="_blank"
rel="noreferrer" 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"> <div className="flex items-center gap-x-1 max-w-full overflow-hidden">
<h3 className="truncate text-ellipsis whitespace-nowrap overflow-hidden w-full"> <h3 className="truncate text-ellipsis whitespace-nowrap overflow-hidden w-full">
@ -151,22 +151,26 @@ function CitationDetailModal({ source, onClose }) {
</div> </div>
</a> </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)} {truncate(title, 45)}
</h3> </h3>
)} )}
</div> </div>
{references > 1 && ( {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. Referenced {references} times.
</p> </p>
)} )}
<button <button
onClick={onClose} onClick={onClose}
type="button" 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> </button>
</div> </div>
<div <div
@ -176,28 +180,31 @@ function CitationDetailModal({ source, onClose }) {
<div className="py-7 px-9 space-y-2 flex-col"> <div className="py-7 px-9 space-y-2 flex-col">
{chunks.map(({ text, score }, idx) => ( {chunks.map(({ text, score }, idx) => (
<Fragment key={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"> <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))} {HTMLDecode(omitChunkHeader(text))}
</p> </p>
{!!score && ( {!!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 <div
data-tooltip-id="similarity-score" 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.`} 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" className="flex items-center gap-x-1"
> >
<Info size={14} /> <Info size={14} />
<p>{toPercentString(score)} match</p> <p>
{toPercentString(score)}{" "}
{t("chat_window.similarity_match")}
</p>
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
{idx !== chunks.length - 1 && ( {idx !== chunks.length - 1 && (
<hr className="border-theme-modal-border" /> <hr className="border-zinc-700 light:border-slate-300" />
)} )}
</Fragment> </Fragment>
))} ))}
@ -228,7 +235,7 @@ const supportedSources = [
* @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options * @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options
* @returns {{isUrl: boolean, text: string, href: string, icon: string}} * @returns {{isUrl: boolean, text: string, href: string, icon: string}}
*/ */
function parseChunkSource({ title = "", chunks = [] }) { export function parseChunkSource({ title = "", chunks = [] }) {
const nullResponse = { const nullResponse = {
isUrl: false, isUrl: false,
text: null, text: null,
@ -315,33 +322,3 @@ function parseChunkSource({ title = "", chunks = [] }) {
} }
return nullResponse; 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}> <div className="mt-2 -ml-0.5 relative" ref={menuRef}>
<button <button
onClick={toggleMenu} 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-id="action-menu"
data-tooltip-content={t("chat_window.more_actions")} data-tooltip-content={t("chat_window.more_actions")}
aria-label={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 { useState, useEffect, useRef } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; 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_prompt")
: t("chat_window.edit_response") : 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")}`} aria-label={`Edit ${role === "user" ? t("chat_window.edit_prompt") : t("chat_window.edit_response")}`}
> >
<Pencil <Pencil size={21} className="mb-1" />
color="var(--theme-sidebar-footer-icon-fill)"
size={21}
className="mb-1"
/>
</button> </button>
</div> </div>
); );
@ -75,17 +71,30 @@ export function EditMessageForm({
saveChanges, saveChanges,
}) { }) {
const formRef = useRef(null); const formRef = useRef(null);
const { t } = useTranslation();
function handleSaveMessage(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const form = new FormData(e.target); const editedMessage = formRef.current.value;
const editedMessage = form.get("editedMessage");
saveChanges({ editedMessage, chatId, role, attachments }); saveChanges({ editedMessage, chatId, role, attachments });
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } }) 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() { function cancelEdits() {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } }) new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
@ -94,36 +103,91 @@ export function EditMessageForm({
} }
useEffect(() => { useEffect(() => {
if (!formRef || !formRef.current) return; if (!formRef?.current) return;
formRef.current.focus(); formRef.current.focus();
adjustTextArea({ target: formRef.current }); 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 ( return (
<form onSubmit={handleSaveMessage} className="flex flex-col w-full"> <form
onSubmit={handleSubmit}
className="flex flex-col w-full max-w-[650px]"
>
<textarea <textarea
ref={formRef} ref={formRef}
name="editedMessage" name="editedMessage"
spellCheck={Appearance.get("enableSpellCheck")} 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} defaultValue={message}
onChange={adjustTextArea} onChange={adjustTextArea}
/> />
<div className="mt-3 flex justify-center"> <EditActionBar onCancel={cancelEdits} />
<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>
</form> </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 { formatDateTimeAsMoment } from "@/utils/directories";
import { numberWithCommas } from "@/utils/numbers"; import { numberWithCommas } from "@/utils/numbers";
import React, { useEffect, useState, useContext } from "react"; import React, { useEffect, useState, useContext } from "react";
import { isMobile } from "react-device-detect";
const MetricsContext = React.createContext(); const MetricsContext = React.createContext();
const SHOW_METRICS_KEY = "anythingllm_show_chat_metrics"; const SHOW_METRICS_KEY = "anythingllm_show_chat_metrics";
const SHOW_METRICS_EVENT = "anythingllm_show_metrics_change"; 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 // Inherit the showMetricsAutomatically state from the MetricsProvider so the state is shared across all chats
const { showMetricsAutomatically, setShowMetricsAutomatically } = const { showMetricsAutomatically, setShowMetricsAutomatically } =
useContext(MetricsContext); useContext(MetricsContext);
if (!metrics?.duration || !metrics?.outputTps) return null; if (!metrics?.duration || !metrics?.outputTps || isMobile) return null;
return ( return (
<button <button
@ -128,9 +129,9 @@ export default function RenderMetrics({ metrics = {} }) {
? "Click to only show metrics when hovering" ? "Click to only show metrics when hovering"
: "Click to show metrics as soon as they are available" : "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)} {buildMetricsString(metrics)}
</p> </p>
</button> </button>

View File

@ -65,7 +65,7 @@ export default function AsyncTTSMessage({ slug, chatId }) {
? t("pause_tts_speech_message") ? t("pause_tts_speech_message")
: t("chat_window.tts_speak_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"} aria-label={speaking ? "Pause speech" : "Speak message"}
> >
{speaking ? ( {speaking ? (

View File

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

View File

@ -18,7 +18,6 @@ const Actions = ({
isEditing, isEditing,
role, role,
metrics = {}, metrics = {},
alignmentCls = "",
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
@ -30,15 +29,21 @@ const Actions = ({
}; };
return ( 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]"> <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]"> <div className="md:group-hover:opacity-100 transition-all duration-300 md:opacity-0 flex justify-start items-center gap-x-[8px]">
<EditMessageAction <div
chatId={chatId} className={`flex justify-start items-center gap-x-[8px] ${role === "user" ? "flex-row-reverse" : ""}`}
role={role} >
isEditing={isEditing} <CopyMessage message={message} />
/> <EditMessageAction
chatId={chatId}
role={role}
isEditing={isEditing}
/>
</div>
{isLastMessage && !isEditing && ( {isLastMessage && !isEditing && (
<RegenerateMessage <RegenerateMessage
regenerateMessage={regenerateMessage} regenerateMessage={regenerateMessage}
@ -80,11 +85,10 @@ function FeedbackButton({
onClick={handleFeedback} onClick={handleFeedback}
data-tooltip-id="feedback-button" data-tooltip-id="feedback-button"
data-tooltip-content={tooltipContent} data-tooltip-content={tooltipContent}
className="text-zinc-300" className="text-zinc-300 light:text-slate-500"
aria-label={tooltipContent} aria-label={tooltipContent}
> >
<IconComponent <IconComponent
color="var(--theme-sidebar-footer-icon-fill)"
size={20} size={20}
className="mb-1" className="mb-1"
weight={isSelected ? "fill" : "regular"} weight={isSelected ? "fill" : "regular"}
@ -105,21 +109,13 @@ function CopyMessage({ message }) {
onClick={() => copyText(message)} onClick={() => copyText(message)}
data-tooltip-id="copy-assistant-text" data-tooltip-id="copy-assistant-text"
data-tooltip-content={t("chat_window.copy")} 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")} aria-label={t("chat_window.copy")}
> >
{copied ? ( {copied ? (
<Check <Check size={20} className="mb-1" />
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
/>
) : ( ) : (
<Copy <Copy size={20} className="mb-1" />
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
/>
)} )}
</button> </button>
</div> </div>
@ -136,15 +132,10 @@ function RegenerateMessage({ regenerateMessage, chatId }) {
onClick={() => regenerateMessage(chatId)} onClick={() => regenerateMessage(chatId)}
data-tooltip-id="regenerate-assistant-text" data-tooltip-id="regenerate-assistant-text"
data-tooltip-content={t("chat_window.regenerate_response")} 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")} aria-label={t("chat_window.regenerate")}
> >
<ArrowsClockwise <ArrowsClockwise size={20} className="mb-1" weight="fill" />
color="var(--theme-sidebar-footer-icon-fill)"
size={20}
className="mb-1"
weight="fill"
/>
</button> </button>
</div> </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 { Info, Warning } from "@phosphor-icons/react";
import UserIcon from "../../../../UserIcon";
import Actions from "./Actions"; import Actions from "./Actions";
import renderMarkdown from "@/utils/chat/markdown"; import renderMarkdown from "@/utils/chat/markdown";
import { userFromStorage } from "@/utils/request";
import Citations from "../Citation"; import Citations from "../Citation";
import { v4 } from "uuid"; import { v4 } from "uuid";
import DOMPurify from "@/utils/chat/purify"; import DOMPurify from "@/utils/chat/purify";
@ -36,7 +34,6 @@ const HistoricalMessage = ({
saveEditedMessage, saveEditedMessage,
forkThread, forkThread,
metrics = {}, metrics = {},
alignmentCls = "",
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isEditing } = useEditMessage({ chatId, role }); const { isEditing } = useEditMessage({ chatId, role });
@ -53,91 +50,120 @@ const HistoricalMessage = ({
const isRefusalMessage = const isRefusalMessage =
role === "assistant" && message === chatQueryRefusalResponse(workspace); role === "assistant" && message === chatQueryRefusalResponse(workspace);
if (completeDelete) return null;
if (!!error) { if (!!error) {
return ( return (
<div <div key={uuid} className="flex justify-start w-full">
key={uuid} <div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
className={`flex justify-center items-end w-full bg-theme-bg-chat`} <div className="p-2 rounded-lg bg-red-50 text-red-500">
> <span className="inline-block">
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> <Warning className="h-4 w-4 mb-1 inline-block" /> Could not
<div className={`flex gap-x-5 ${alignmentCls}`}> respond to message.
<ProfileImage role={role} workspace={workspace} /> </span>
<div className="p-2 rounded-lg bg-red-50 text-red-500"> <p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
<span className="inline-block"> {error}
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not </p>
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>
</div> </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 ( return (
<div <div
key={uuid} key={uuid}
onAnimationEnd={onEndAnimation} onAnimationEnd={onEndAnimation}
className={`${ className={`${isDeleted ? "animate-remove" : ""} flex justify-end w-full group`}
isDeleted ? "animate-remove" : "" >
} flex justify-center items-end w-full group bg-theme-bg-chat`} <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">
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> <TruncatableContent>
<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">
<RenderChatContent <RenderChatContent
role={role} role={role}
message={message} message={message}
messageId={uuid} 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} /> <ChatAttachments attachments={attachments} />
</div> </TruncatableContent>
)} </div>
</div> <Actions
<div className="flex gap-x-5 ml-14"> 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 <Actions
message={message} message={message}
feedbackScore={feedbackScore} feedbackScore={feedbackScore}
@ -149,7 +175,6 @@ const HistoricalMessage = ({
role={role} role={role}
forkThread={forkThread} forkThread={forkThread}
metrics={metrics} metrics={metrics}
alignmentCls={alignmentCls}
/> />
</div> </div>
{role === "assistant" && <Citations sources={sources} />} {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( export default memo(
HistoricalMessage, HistoricalMessage,
// Skip re-render the historical message: // Skip re-render the historical message:
@ -199,18 +201,73 @@ export default memo(
function ChatAttachments({ attachments = [] }) { function ChatAttachments({ attachments = [] }) {
if (!attachments.length) return null; if (!attachments.length) return null;
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-4 mt-4">
{attachments.map((item) => ( {attachments.map((item) => (
<img <img
alt={`Attachment: ${item.name}`}
key={item.name} key={item.name}
src={item.contentString} src={item.contentString}
className="max-w-[300px] rounded-md" className="w-[120px] h-[120px] object-cover rounded-lg"
/> />
))} ))}
</div> </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( const RenderChatContent = memo(
({ role, message, messageId }) => { ({ role, message, messageId }) => {
// If the message is not from the assistant, we can render it directly // If the message is not from the assistant, we can render it directly
@ -218,7 +275,7 @@ const RenderChatContent = memo(
if (role !== "assistant") if (role !== "assistant")
return ( return (
<span <span
className="flex flex-col gap-y-1" className="flex flex-col gap-y-1 text-white light:text-slate-900"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)), __html: DOMPurify.sanitize(renderMarkdown(message)),
}} }}
@ -252,7 +309,7 @@ const RenderChatContent = memo(
<ThoughtChainComponent content={thoughtChain} messageId={messageId} /> <ThoughtChainComponent content={thoughtChain} messageId={messageId} />
)} )}
<span <span
className="flex flex-col gap-y-1" className="flex flex-col gap-y-1 text-white light:text-slate-900"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(msgToRender)), __html: DOMPurify.sanitize(renderMarkdown(msgToRender)),
}} }}

View File

@ -1,7 +1,6 @@
/* eslint-disable react-hooks/refs */ /* eslint-disable react-hooks/refs */
import { memo, useRef, useEffect } from "react"; import { memo, useRef, useEffect } from "react";
import { Warning } from "@phosphor-icons/react"; import { Warning } from "@phosphor-icons/react";
import UserIcon from "../../../../UserIcon";
import renderMarkdown from "@/utils/chat/markdown"; import renderMarkdown from "@/utils/chat/markdown";
import Citations from "../Citation"; import Citations from "../Citation";
import { import {
@ -11,28 +10,14 @@ import {
ThoughtChainComponent, ThoughtChainComponent,
} from "../ThoughtContainer"; } from "../ThoughtContainer";
const PromptReply = ({ const PromptReply = ({ uuid, reply, pending, error, sources = [] }) => {
uuid,
reply,
pending,
error,
workspace,
sources = [],
}) => {
const assistantBackgroundColor = "bg-theme-bg-chat";
if (!reply && sources.length === 0 && !pending && !error) return null; if (!reply && sources.length === 0 && !pending && !error) return null;
if (pending) { if (pending) {
return ( return (
<div <div className="flex justify-start w-full">
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`} <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 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> </div>
</div> </div>
); );
@ -40,61 +25,32 @@ const PromptReply = ({
if (error) { if (error) {
return ( return (
<div <div className="flex justify-start w-full">
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`} <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">
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> <Warning className="h-4 w-4 mb-1 inline-block" /> Could not respond
<div className="flex gap-x-5"> to message.
<WorkspaceProfileImage workspace={workspace} /> <span className="text-xs">Reason: {error || "unknown"}</span>
<span </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> </div>
</div> </div>
); );
} }
return ( return (
<div <div key={uuid} className="flex justify-start w-full">
key={uuid} <div className="py-4 pl-0 pr-4 flex flex-col w-full">
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`} <RenderAssistantChatContent
> key={`${uuid}-prompt-reply-content`}
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col"> message={reply}
<div className="flex gap-x-5"> messageId={uuid}
<WorkspaceProfileImage workspace={workspace} /> />
<RenderAssistantChatContent
key={`${uuid}-prompt-reply-content`}
message={reply}
messageId={uuid}
/>
</div>
<Citations sources={sources} /> <Citations sources={sources} />
</div> </div>
</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 }) { function RenderAssistantChatContent({ message, messageId }) {
const contentRef = useRef(""); const contentRef = useRef("");
const thoughtChainRef = useRef(null); const thoughtChainRef = useRef(null);

View File

@ -15,22 +15,25 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
} }
return ( return (
<div className="flex justify-center w-full"> <div className="flex justify-center w-full pr-4">
<div className="w-full max-w-[80%] flex flex-col"> <div className="w-full flex flex-col">
<div className=" w-full max-w-[800px]"> <div className="w-full">
<div <div
onClick={handleExpandClick} onClick={handleExpandClick}
style={{ borderRadius: "6px" }} style={{
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`} 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 ? ( {isThinking ? (
<video <video
autoPlay autoPlay
loop loop
muted muted
playsInline 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-id="agent-thinking"
data-tooltip-content="Agent is thinking..." data-tooltip-content="Agent is thinking..."
aria-label="Agent is thinking..." aria-label="Agent is thinking..."
@ -41,57 +44,53 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
<img <img
src={AgentStatic} src={AgentStatic}
alt="Agent complete" 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-id="agent-thinking"
data-tooltip-content="Agent has finished thinking" data-tooltip-content="Agent has finished thinking"
aria-label="Agent has finished thinking" aria-label="Agent has finished thinking"
/> />
)} )}
</div> </div>
<div className="flex-1 min-w-0"> {previousThoughts?.length > 0 && (
<div <button
className={`overflow-hidden transition-all duration-300 ease-in-out ${isExpanded ? "" : "max-h-6"}`} 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"> <CaretDown
{!isExpanded ? ( className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
<span className="block w-full truncate mt-[2px]"> />
{currentThought.content} </button>
</span> )}
) : ( <div
<> className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? "" : "overflow-hidden max-h-[18px]"}`}
{previousThoughts.map((thought, index) => ( >
<div <div className="text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px]">
key={`cot-${thought.uuid || index}`} {!isExpanded ? (
className="mb-2" <span className="block w-full truncate">
> {currentThought.content}
{thought.content} </span>
</div> ) : (
))} <>
<div>{currentThought.content}</div> {previousThoughts.map((thought, index) => (
</> <div
)} key={`cot-${thought.uuid || index}`}
</div> className="mb-2"
>
{thought.content}
</div>
))}
<div>{currentThought.content}</div>
</>
)}
</div> </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> </div>
</div> </div>

View File

@ -141,50 +141,63 @@ export const ThoughtChainComponent = forwardRef(
} }
return ( return (
<div className="flex justify-start items-end transition-all duration-200 w-full md:max-w-[800px]"> <div className="flex justify-center w-full">
<div className="pb-2 w-full flex gap-x-5 flex-col relative"> <div className="w-full flex flex-col">
<div <div className="w-full">
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 <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 ? ( <div className="absolute top-4 left-4 w-[18px] h-[18px]">
<> {isThinking || isComplete ? (
<video <>
autoPlay <video
loop autoPlay
muted loop
playsInline muted
className={`w-7 h-7 transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`} playsInline
data-tooltip-id="cot-thinking" className={`w-[18px] h-[18px] scale-[115%] transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-content="Model is thinking..." data-tooltip-id="cot-thinking"
aria-label="Model is thinking..." data-tooltip-content="Model is thinking..."
> aria-label="Model is thinking..."
<source src={ThinkingAnimation} type="video/webm" /> >
</video> <source src={ThinkingAnimation} type="video/webm" />
<img </video>
src={ThinkingStatic} <img
alt="Thinking complete" src={ThinkingStatic}
className={`w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`} alt="Thinking complete"
data-tooltip-id="cot-thinking" className={`w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-content="Model has finished thinking" data-tooltip-id="cot-thinking"
aria-label="Model has finished thinking" data-tooltip-content="Model has finished thinking"
/> aria-label="Model has finished thinking"
</> />
) : null} </>
</div> ) : null}
<div className="flex-1 min-w-0"> </div>
<div {canExpand && (
className={`overflow-hidden transition-all transform duration-300 ease-in-out origin-top ${isExpanded ? "" : "max-h-6"}`} <button
> onClick={handleExpandClick}
<div className="absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors"
className={`text-theme-text-secondary font-mono leading-6 ${isExpanded ? "-ml-[5.5px] -mt-[4px]" : "mt-[2px]"}`} 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 <span
className={`block w-full ${!isExpanded ? "truncate" : ""}`} className={`block w-full ${!isExpanded ? "truncate" : ""}`}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -198,25 +211,6 @@ export const ThoughtChainComponent = forwardRef(
</div> </div>
</div> </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> </div>
</div> </div>

View File

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

View File

@ -1,5 +1,6 @@
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
/** /**
* Set the tooltips for the chat container in bulk. * Set the tooltips for the chat container in bulk.
@ -16,6 +17,8 @@ import { createPortal } from "react-dom";
* @returns * @returns
*/ */
export function ChatTooltips() { export function ChatTooltips() {
const { t } = useTranslation();
return ( return (
<> <>
<Tooltip <Tooltip
@ -96,6 +99,13 @@ export function ChatTooltips() {
delayShow={300} delayShow={300}
className="tooltip !text-xs" 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 /> <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 { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
@ -98,15 +98,16 @@ export default function AttachItem({
type="button" type="button"
onClick={handleClick} onClick={handleClick}
onPointerEnter={fetchFiles} 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"> <div className="relative">
<PaperclipHorizontal <Plus
color="var(--theme-sidebar-footer-icon-fill)" size={18}
className="w-[20px] h-[20px] pointer-events-none text-white rotate-90 -scale-y-100" 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 && ( {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} {files.length}
</div> </div>
)} )}

View File

@ -1,7 +1,6 @@
import useGetProviderModels, { import useGetProviderModels, {
DISABLED_PROVIDERS, DISABLED_PROVIDERS,
} from "@/hooks/useGetProvidersModels"; } from "@/hooks/useGetProvidersModels";
import { useTranslation } from "react-i18next";
export default function ChatModelSelection({ export default function ChatModelSelection({
provider, provider,
@ -11,110 +10,81 @@ export default function ChatModelSelection({
}) { }) {
const { defaultModels, customModels, loading } = const { defaultModels, customModels, loading } =
useGetProviderModels(provider); useGetProviderModels(provider);
const { t } = useTranslation();
if (DISABLED_PROVIDERS.includes(provider)) return null; if (DISABLED_PROVIDERS.includes(provider)) return null;
if (loading) { if (loading) {
return ( return (
<div> <select
<div className="flex flex-col"> required={true}
<label htmlFor="name" className="block input-label"> disabled={true}
{t("chat_window.workspace_llm_manager.available_models", { 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"
provider, >
})} <option disabled={true} selected={true}>
</label> -- waiting for models --
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> </option>
{t( </select>
"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>
); );
} }
return ( return (
<div> <select
<div className="flex flex-col"> id="workspace-llm-model-select"
<label htmlFor="name" className="block input-label"> required={true}
{t("chat_window.workspace_llm_manager.available_models", { value={selectedLLMModel}
provider, 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> </optgroup>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> )}
{t("chat_window.workspace_llm_manager.available_models_description")} {Array.isArray(customModels) && customModels.length > 0 && (
</p> <optgroup label="Discovered models">
</div> {customModels.map((model) => {
return (
<select <option
id="workspace-llm-model-select" key={model.id}
required={true} value={model.id}
value={selectedLLMModel} selected={selectedLLMModel === model.id}
onChange={(e) => { >
setHasChanges(true); {model.id}
setSelectedLLMModel(e.target.value); </option>
}} );
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" })}
> </optgroup>
{defaultModels.length > 0 && ( )}
<optgroup label="General models"> {/* For providers like TogetherAi where we partition model by creator entity. */}
{defaultModels.map((model) => { {!Array.isArray(customModels) && Object.keys(customModels).length > 0 && (
return ( <>
<option {Object.entries(customModels).map(([organization, models]) => (
key={model} <optgroup key={organization} label={organization}>
value={model} {models.map((model) => (
selected={selectedLLMModel === model}
>
{model}
</option>
);
})}
</optgroup>
)}
{Array.isArray(customModels) && customModels.length > 0 && (
<optgroup label="Discovered models">
{customModels.map((model) => {
return (
<option <option
key={model.id} key={model.id}
value={model.id} value={model.id}
selected={selectedLLMModel === model.id} selected={selectedLLMModel === model.id}
> >
{model.id} {model.name}
</option> </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>
))} ))}
</> </optgroup>
)} ))}
</select> </>
</div> )}
</select>
); );
} }

View File

@ -1,3 +1,4 @@
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function LLMSelectorSidePanel({ export default function LLMSelectorSidePanel({
@ -9,31 +10,42 @@ export default function LLMSelectorSidePanel({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="w-[40%] h-full flex flex-col gap-y-1 border-r-2 border-theme-modal-border py-2 px-[5px]"> <div className="w-[40%] h-full flex flex-col gap-4 p-2 border-r border-zinc-700 light:border-slate-300">
<input <div className="relative shrink-0 mx-2">
id="llm-search-input" <MagnifyingGlass
type="search" size={14}
placeholder={t("chat_window.workspace_llm_manager.search")} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400 light:text-slate-400"
onChange={onSearchChange} weight="bold"
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" />
/> <input
<div className="flex flex-col gap-y-2 overflow-y-scroll "> 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) => ( {availableProviders.map((llm) => (
<button <button
key={llm.value} key={llm.value}
type="button" type="button"
data-llm-value={llm.value} 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)} onClick={() => onProviderClick(llm.value)}
> >
<img <img
src={llm.logo} src={llm.logo}
alt={`${llm.name} logo`} alt={`${llm.name} logo`}
className="w-6 h-6 rounded-md" className="w-6 h-6 rounded"
/> />
<div className="flex flex-col"> <span className="text-sm text-white light:text-slate-900">
<div className="text-xs text-theme-text-primary">{llm.name}</div> {llm.name}
</div> </span>
</button> </button>
))} ))}
</div> </div>

View File

@ -1,6 +1,6 @@
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { X } from "@phosphor-icons/react"; import { X, WarningCircle } from "@phosphor-icons/react";
import System from "@/models/system"; import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -93,17 +93,23 @@ export function NoSetupWarning({ showing, onSetupClick }) {
if (!showing) return null; if (!showing) return null;
return ( return (
<button <div className="flex items-start gap-1.5">
type="button" <WarningCircle
onClick={onSetupClick} size={16}
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" className="text-white light:text-slate-800 shrink-0 mt-0.5"
> />
<p className="text-sm text-center"> <p className="text-[13px] text-white light:text-slate-800 leading-5">
<b>{t("chat_window.workspace_llm_manager.missing_credentials")}</b> {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>
<p className="text-xs text-center"> </div>
{t("chat_window.workspace_llm_manager.missing_credentials_description")}
</p>
</button>
); );
} }

View File

@ -16,7 +16,10 @@ import showToast from "@/utils/toast";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import System from "@/models/system"; 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 } = useParams();
const slug = urlSlug ?? workspaceSlug; const slug = urlSlug ?? workspaceSlug;
const { t } = useTranslation(); const { t } = useTranslation();
@ -36,14 +39,22 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
setLoading(true); setLoading(true);
Promise.all([Workspace.bySlug(slug), System.keys()]) Promise.all([Workspace.bySlug(slug), System.keys()])
.then(([workspace, systemSettings]) => { .then(([workspace, systemSettings]) => {
const selectedLLMProvider = const savedProvider =
workspace.chatProvider ?? systemSettings.LLMProvider; workspace.chatProvider ?? systemSettings.LLMProvider;
const selectedLLMModel = workspace.chatModel ?? systemSettings.LLMModel; const savedModel = workspace.chatModel ?? systemSettings.LLMModel;
const providerToSelect = initialProvider ?? savedProvider;
setSettings(systemSettings); setSettings(systemSettings);
setSelectedLLMProvider(selectedLLMProvider); setSelectedLLMProvider(providerToSelect);
autoScrollToSelectedLLMProvider(selectedLLMProvider); autoScrollToSelectedLLMProvider(providerToSelect);
setSelectedLLMModel(selectedLLMModel); setSelectedLLMModel(savedModel);
if (initialProvider && initialProvider !== savedProvider) {
setHasChanges(true);
setMissingCredentials(
hasMissingCredentials(systemSettings, initialProvider)
);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [slug]); }, [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) { if (loading) {
return ( return (
<div <div
id="llm-selector-modal" 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} /> <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")} {t("chat_window.workspace_llm_manager.loading_workspace_settings")}
</p> </p>
</div> </div>
@ -102,17 +117,36 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
} }
return ( return (
<div <div id="llm-selector-modal" className="w-full h-[388px] flex">
id="llm-selector-modal"
className="w-full h-[500px] p-0 overflow-y-scroll flex"
>
<LLMSelectorSidePanel <LLMSelectorSidePanel
availableProviders={availableProviders} availableProviders={availableProviders}
selectedLLMProvider={selectedLLMProvider} selectedLLMProvider={selectedLLMProvider}
onSearchChange={handleSearch} onSearchChange={handleSearch}
onProviderClick={handleProviderSelection} 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 <NoSetupWarning
showing={missingCredentials} showing={missingCredentials}
onSetupClick={() => { onSetupClick={() => {
@ -128,18 +162,12 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
); );
}} }}
/> />
<ChatModelSelection {hasChanges && !missingCredentials && (
provider={selectedLLMProvider}
setHasChanges={setHasChanges}
selectedLLMModel={selectedLLMModel}
setSelectedLLMModel={setSelectedLLMModel}
/>
{hasChanges && (
<button <button
type="button" type="button"
disabled={saving} disabled={saving}
onClick={handleSave} 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 {saving
? t("chat_window.workspace_llm_manager.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)`} data-tooltip-content={`${t("chat_window.microphone")} (CTRL + M)`}
aria-label={t("chat_window.microphone")} aria-label={t("chat_window.microphone")}
onClick={listening ? endSTTSession : startSTTSession} 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 ${ 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 ? "!opacity-100" : "" listening ? "bg-zinc-700 light:bg-slate-200" : ""
}`} }`}
> >
<Microphone <Microphone
weight="regular" weight="regular"
color="var(--theme-sidebar-footer-icon-fill)" size={18}
className={`w-[20px] h-[20px] pointer-events-none text-theme-text-primary ${ 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" : "" listening
? "animate-pulse-glow !text-white light:!text-slate-800"
: ""
}`} }`}
/> />
<Tooltip <Tooltip

View File

@ -1,8 +1,9 @@
import { ABORT_STREAM_EVENT } from "@/utils/chat"; import { ABORT_STREAM_EVENT } from "@/utils/chat";
import { Stop } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next";
export default function StopGenerationButton() { export default function StopGenerationButton() {
const { t } = useTranslation();
function emitHaltEvent() { function emitHaltEvent() {
window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT)); window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));
} }
@ -13,14 +14,11 @@ export default function StopGenerationButton() {
type="button" type="button"
onClick={emitHaltEvent} onClick={emitHaltEvent}
data-tooltip-id="stop-generation-button" data-tooltip-id="stop-generation-button"
data-tooltip-content="Stop generating response" data-tooltip-content={t("chat_window.stop_generating")}
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" 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" aria-label="Stop generating"
> >
<Stop <div className="w-3.5 h-3.5 rounded-[4px] bg-zinc-800 light:bg-white" />
className="w-[12px] h-[12px] light:text-white text-black"
weight="fill"
/>
</button> </button>
<Tooltip <Tooltip
id="stop-generation-button" 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 { useState } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from "."; import { CMD_REGEX } from "./constants";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function AddPresetModal({ isOpen, onClose, onSave }) { export default function AddPresetModal({ isOpen, onClose, onSave }) {

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { CMD_REGEX } from "."; import { CMD_REGEX } from "./constants";
export default function EditPresetModal({ export default function EditPresetModal({
isOpen, 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 { useState, useRef, useEffect } from "react";
import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
} from "./SlashCommands";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import { ArrowUp } from "@phosphor-icons/react"; import { ArrowUp, At } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton"; import StopGenerationButton from "./StopGenerationButton";
import AvailableAgentsButton, {
AvailableAgents,
useAvailableAgents,
} from "./AgentMenu";
import TextSizeButton from "./TextSizeMenu";
import LLMSelectorAction from "./LLMSelector/action";
import SpeechToText from "./SpeechToText"; import SpeechToText from "./SpeechToText";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import AttachmentManager from "./Attachments"; import AttachmentManager from "./Attachments";
@ -25,6 +15,9 @@ import useTextSize from "@/hooks/useTextSize";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import usePromptInputStorage from "@/hooks/usePromptInputStorage"; 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_ID = "primary-prompt-input";
export const PROMPT_INPUT_EVENT = "set_prompt_input"; export const PROMPT_INPUT_EVENT = "set_prompt_input";
@ -50,15 +43,18 @@ export default function PromptInput({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { isDisabled } = useIsDisabled(); const { isDisabled } = useIsDisabled();
const agentSessionActive = useIsAgentSessionActive();
const [promptInput, setPromptInput] = useState(""); const [promptInput, setPromptInput] = useState("");
const { showAgents, setShowAgents } = useAvailableAgents(); const [showTools, setShowTools] = useState(false);
const { showSlashCommand, setShowSlashCommand } = useSlashCommands(); const autoOpenedToolsRef = useRef(false);
const toolsHighlightRef = useRef(-1);
const formRef = useRef(null); const formRef = useRef(null);
const textareaRef = useRef(null); const textareaRef = useRef(null);
const [_, setFocused] = useState(false); const [_, setFocused] = useState(false);
const undoStack = useRef([]); const undoStack = useRef([]);
const redoStack = useRef([]); const redoStack = useRef([]);
const { textSizeClass } = useTextSize(); const { textSizeClass } = useTextSize();
const [searchParams] = useSearchParams();
// Synchronizes prompt input value with localStorage, scoped to the current thread. // Synchronizes prompt input value with localStorage, scoped to the current thread.
usePromptInputStorage({ usePromptInputStorage({
@ -66,6 +62,18 @@ export default function PromptInput({
setPromptInput, 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 * 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 * 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) { function handlePromptUpdate(e) {
const { messageContent, writeMode = "replace" } = e?.detail ?? {}; const { messageContent, writeMode = "replace" } = e?.detail ?? {};
if (writeMode === "append") setPromptInput((prev) => prev + messageContent); if (writeMode === "append") setPromptInput((prev) => prev + messageContent);
else if (writeMode === "prepend")
setPromptInput((prev) => messageContent + " " + prev);
else setPromptInput(messageContent ?? ""); else setPromptInput(messageContent ?? "");
} }
@ -106,7 +116,10 @@ export default function PromptInput({
const debouncedSaveState = debounce(saveCurrentState, 250); const debouncedSaveState = debounce(saveCurrentState, 250);
function handleSubmit(e) { function handleSubmit(e) {
// Ignore submits from portaled modals (slash command preset forms)
if (e.target !== e.currentTarget) return;
setFocused(false); setFocused(false);
setShowTools(false);
submit(e); submit(e);
} }
@ -115,31 +128,63 @@ export default function PromptInput({
textareaRef.current.style.height = "auto"; 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 * Capture enter key press to handle submission, redo, or undo
* via keyboard shortcuts * via keyboard shortcuts
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
function captureEnterOrUndo(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 // Is simple enter key press w/o shift key
if (event.keyCode === 13 && !event.shiftKey) { if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled
setShowTools(false);
return submit(event); return submit(event);
} }
@ -252,10 +297,15 @@ export default function PromptInput({
function handleChange(e) { function handleChange(e) {
debouncedSaveState(-1); debouncedSaveState(-1);
watchForSlash(e);
watchForAt(e);
adjustTextArea(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 ( return (
@ -263,23 +313,9 @@ export default function PromptInput({
className={ className={
centered centered
? "w-full relative flex justify-center items-center" ? "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 <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={ className={
@ -291,80 +327,72 @@ export default function PromptInput({
<div <div
className={`flex items-center rounded-lg md:w-full ${centered ? "mb-0" : "mb-4"}`} 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"> <div className="relative w-[95vw] md:w-[750px]">
<AttachmentManager attachments={attachments} /> <ToolsMenu
<div className="flex items-center mx-[7px]"> showing={showTools}
<textarea setShowing={setShowTools}
id={PROMPT_INPUT_ID} sendCommand={sendCommand}
ref={textareaRef} promptRef={textareaRef}
onChange={handleChange} centered={centered}
onKeyDown={captureEnterOrUndo} highlightedIndexRef={toolsHighlightRef}
onPaste={(e) => { />
saveCurrentState(); <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">
handlePasteEvent(e); <AttachmentManager attachments={attachments} />
}} <div className="flex items-center">
required={true} <textarea
onFocus={() => setFocused(true)} id={PROMPT_INPUT_ID}
onBlur={(e) => { ref={textareaRef}
setFocused(false); onChange={handleChange}
adjustTextArea(e); onKeyDown={captureEnterOrUndo}
}} onPaste={(e) => {
value={promptInput} saveCurrentState();
spellCheck={Appearance.get("enableSpellCheck")} handlePasteEvent(e);
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")} required={true}
/> onFocus={() => setFocused(true)}
</div> onBlur={(e) => {
<div className="flex justify-between items-center pt-3.5 pb-3 mx-[7px]"> setFocused(false);
<div className="flex gap-x-2 items-center h-5 -ml-[4.5px]"> adjustTextArea(e);
<AttachItem }}
workspaceSlug={workspaceSlug} value={promptInput}
workspaceThreadSlug={threadSlug} 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>
<div className="flex gap-x-2 items-center h-5"> <div className="flex justify-between items-center pt-3.5 pb-3">
<SpeechToText sendCommand={sendCommand} /> <div className="flex items-center gap-x-0.25">
{isStreaming ? ( <div className="flex items-center gap-x-1">
<StopGenerationButton /> <AttachItem
) : ( workspaceSlug={workspaceSlug}
<> workspaceThreadSlug={threadSlug}
<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"
/> />
</> <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> </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 * 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. * 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, websocketURI,
AGENT_SESSION_END, AGENT_SESSION_END,
AGENT_SESSION_START, AGENT_SESSION_START,
setAgentSessionActive,
} from "@/utils/chat/agent"; } from "@/utils/chat/agent";
import DnDFileUploaderWrapper from "./DnDWrapper"; import DnDFileUploaderWrapper from "./DnDWrapper";
import SpeechRecognition, { import SpeechRecognition, {
@ -24,11 +25,15 @@ import { ChatTooltips } from "./ChatTooltips";
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics"; import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll"; import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
import { PENDING_HOME_MESSAGE } from "@/utils/constants"; import { PENDING_HOME_MESSAGE } from "@/utils/constants";
import { clearPromptInputDraft } from "@/hooks/usePromptInputStorage";
import { safeJsonParse } from "@/utils/request"; import { safeJsonParse } from "@/utils/request";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import QuickActions from "@/components/lib/QuickActions"; import QuickActions from "@/components/lib/QuickActions";
import SuggestedMessages from "@/components/lib/SuggestedMessages"; 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 = [] }) { export default function ChatContainer({ workspace, knownHistory = [] }) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -66,6 +71,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
document.getElementById(PROMPT_INPUT_ID)?.value || ""; document.getElementById(PROMPT_INPUT_ID)?.value || "";
if (!currentMessage) return false; 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 = [ const prevChatHistory = [
...chatHistory, ...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 {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[]} 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 {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} * @returns {void}
*/ */
const sendCommand = async ({ const sendCommand = async ({
@ -134,6 +143,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
return; return;
} }
if (writeMode === "prepend") {
const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
text = currentText + " " + text;
}
// If we are auto-submitting in append mode // 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. // 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. // @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; 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 // If we are auto-submitting
// Then we can replace the current text since this is not accumulating. // Then we can replace the current text since this is not accumulating.
let prevChatHistory; let prevChatHistory;
@ -250,17 +270,20 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
// TODO: Simplify this WSS stuff // TODO: Simplify this WSS stuff
useEffect(() => { useEffect(() => {
let socket = null;
function handleWSS() { function handleWSS() {
try { try {
if (!socketId || !!websocket) return; if (!socketId || !!websocket) return;
const socket = new WebSocket( socket = new WebSocket(
`${websocketURI()}/api/agent-invocation/${socketId}` `${websocketURI()}/api/agent-invocation/${socketId}`
); );
socket.supportsAgentStreaming = false; socket.supportsAgentStreaming = false;
window.addEventListener(ABORT_STREAM_EVENT, () => { window.addEventListener(ABORT_STREAM_EVENT, () => {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
websocket.close(); socket?.close();
}); });
socket.addEventListener("message", (event) => { socket.addEventListener("message", (event) => {
@ -269,6 +292,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
handleSocketResponse(socket, event, setChatHistory); handleSocketResponse(socket, event, setChatHistory);
} catch { } catch {
console.error("Failed to parse data"); console.error("Failed to parse data");
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
socket.close(); socket.close();
} }
@ -276,6 +300,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
}); });
socket.addEventListener("close", (_event) => { socket.addEventListener("close", (_event) => {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
setChatHistory((prev) => [ setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content), ...prev.filter((msg) => !!msg.content),
@ -296,6 +321,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setSocketId(null); setSocketId(null);
}); });
setWebsocket(socket); setWebsocket(socket);
setAgentSessionActive(true);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START)); window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
} catch (e) { } catch (e) {
@ -319,6 +345,14 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
} }
} }
handleWSS(); handleWSS();
return () => {
if (socket) {
setAgentSessionActive(false);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
socket.close();
}
};
}, [socketId]); }, [socketId]);
const isEmpty = const isEmpty =
@ -328,9 +362,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
return ( return (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 />} {isMobile && <SidebarMobileHeader />}
<TextSizeMenu />
<WorkspaceModelPicker workspaceSlug={workspace.slug} />
<DnDFileUploaderWrapper> <DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full items-center justify-center"> <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]"> <div className="flex flex-col items-center w-full max-w-[750px]">
@ -369,35 +405,42 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
} }
return ( return (
<div <SourcesSidebarProvider>
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} <div
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]" 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]"
{isMobile && <SidebarMobileHeader />} >
<DnDFileUploaderWrapper> <TextSizeMenu />
<div className="flex flex-col h-full w-full"> <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">
<div className="contents"> {isMobile && <SidebarMobileHeader />}
<MetricsProvider> <WorkspaceModelPicker workspaceSlug={workspace.slug} />
<ChatHistory <DnDFileUploaderWrapper>
ref={chatHistoryRef} <div className="flex flex-col h-full w-full pb-20 md:pb-0">
history={chatHistory} <div className="contents">
workspace={workspace} <MetricsProvider>
sendCommand={sendCommand} <ChatHistory
updateHistory={setChatHistory} ref={chatHistoryRef}
regenerateAssistantMessage={regenerateAssistantMessage} history={chatHistory}
/> workspace={workspace}
</MetricsProvider> sendCommand={sendCommand}
<PromptInput updateHistory={setChatHistory}
submit={handleSubmit} regenerateAssistantMessage={regenerateAssistantMessage}
isStreaming={loadingResponse} />
sendCommand={sendCommand} </MetricsProvider>
attachments={files} <PromptInput
centered={false} submit={handleSubmit}
/> isStreaming={loadingResponse}
</div> sendCommand={sendCommand}
attachments={files}
centered={false}
/>
</div>
</div>
</DnDFileUploaderWrapper>
<ChatTooltips />
</div> </div>
</DnDFileUploaderWrapper> <SourcesSidebar />
<ChatTooltips /> </div>
</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 * @param {Function} props.setPromptInput - State setter function for prompt input
* @returns {void} * @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 }) { export default function usePromptInputStorage({ promptInput, setPromptInput }) {
const { threadSlug = null, slug: workspaceSlug } = useParams(); const { threadSlug = null, slug: workspaceSlug } = useParams();
useEffect(() => { 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) { @media (prefers-color-scheme: light) {
.sidebar-items:after { .sidebar-items:after {
@ -677,7 +658,7 @@ dialog::backdrop {
} }
.tooltip { .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 { .Toastify__toast-body {

View File

@ -148,12 +148,6 @@ const TRANSLATIONS = {
heading: "اشرح لي", heading: "اشرح لي",
body: "فوائد برنامج إيني ثينك إلْلْمْ", body: "فوائد برنامج إيني ثينك إلْلْمْ",
}, },
pfp: {
title: "صورة الملف الشخصي للمساعد",
description: "تخصيص صورة الملف الشخصي للمساعد لمساحة العمل هذه.",
image: "صورة مساحة العمل",
remove: "إزالة صورة مساحة العمل",
},
delete: { delete: {
title: "حذف مساحة العمل", title: "حذف مساحة العمل",
description: description:
@ -648,8 +642,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "أرسل رسالة", send_message: "أرسل رسالة",
attach_file: "أرفق ملفًا بهذا الدردشة", attach_file: "أرفق ملفًا بهذا الدردشة",
slash: "عرض جميع الأوامر المتاحة للتواصل.",
agents: "عرض جميع الوكلاء المتاحين الذين يمكنك استخدامهم للمحادثة.",
text_size: "تغيير حجم النص.", text_size: "تغيير حجم النص.",
microphone: "اذكر طلبك.", microphone: "اذكر طلبك.",
send: "أرسل رسالة فورية إلى مساحة العمل", send: "أرسل رسالة فورية إلى مساحة العمل",
@ -660,18 +652,11 @@ const TRANSLATIONS = {
regenerate_response: "أعد الرد", regenerate_response: "أعد الرد",
good_response: "رد جيد", good_response: "رد جيد",
more_actions: "إجراءات إضافية", more_actions: "إجراءات إضافية",
hide_citations: "إخفاء المراجع",
show_citations: "عرض المراجع",
fork: "شوكة", fork: "شوكة",
delete: "حذف", delete: "حذف",
save_submit: "حفظ وإرسال",
cancel: "إلغاء", cancel: "إلغاء",
edit_prompt: "اقتراح التحرير", edit_prompt: "اقتراح التحرير",
edit_response: "عدّل الرد", edit_response: "عدّل الرد",
at_agent: "@agent",
default_agent_description: "- الوكيل الافتراضي لهذا المساحة.",
custom_agents_coming_soon: "سيصل وكلاء مخصصون قريباً!",
slash_reset: "/reset",
preset_reset_description: "امسح سجل الدردشة الخاص بك وابدأ محادثة جديدة", preset_reset_description: "امسح سجل الدردشة الخاص بك وابدأ محادثة جديدة",
add_new_preset: "إضافة إعداد مسبق", add_new_preset: "إضافة إعداد مسبق",
command: "أمر", command: "أمر",
@ -693,6 +678,35 @@ const TRANSLATIONS = {
missing_credentials: "هذا المزود لا يمتلك المؤهلات اللازمة!", missing_credentials: "هذا المزود لا يمتلك المؤهلات اللازمة!",
missing_credentials_description: "انقر لإعداد بيانات الاعتماد", 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: { profile_settings: {
edit_account: "تحرير الحساب", edit_account: "تحرير الحساب",
@ -758,10 +772,6 @@ const TRANSLATIONS = {
title: "اسم", title: "اسم",
description: "حدد اسمًا يظهر في صفحة تسجيل الدخول لجميع المستخدمين.", description: "حدد اسمًا يظهر في صفحة تسجيل الدخول لجميع المستخدمين.",
}, },
"chat-message-alignment": {
title: "مواءمة رسائل الدردشة",
description: "حدد وضع محاذاة الرسائل عند استخدام واجهة الدردشة.",
},
"display-language": { "display-language": {
title: "اللغة المعروضة", title: "اللغة المعروضة",
description: description:

View File

@ -164,13 +164,6 @@ const TRANSLATIONS = {
heading: "Vysvětlit mi", heading: "Vysvětlit mi",
body: "výhody AnythingLLM", 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: { delete: {
title: "Smazat pracovní prostor", title: "Smazat pracovní prostor",
description: description:
@ -398,11 +391,6 @@ const TRANSLATIONS = {
description: description:
"Nastavte název, který je zobrazen na přihlašovací stránce všem uživatelům.", "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": { "display-language": {
title: "Zobrazovací jazyk", title: "Zobrazovací jazyk",
description: description:
@ -794,9 +782,6 @@ const TRANSLATIONS = {
attachments_processing: "Přílohy se zpracovávají. Prosím čekejte...", attachments_processing: "Přílohy se zpracovávají. Prosím čekejte...",
send_message: "Odeslat zprávu", send_message: "Odeslat zprávu",
attach_file: "Přiložit soubor k tomuto chatu", 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.", text_size: "Změnit velikost textu.",
microphone: "Mluvit svou výzvu.", microphone: "Mluvit svou výzvu.",
send: "Odeslat zprávu výzvy do pracovního prostoru", send: "Odeslat zprávu výzvy do pracovního prostoru",
@ -806,18 +791,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerovat odpověď", regenerate_response: "Regenerovat odpověď",
good_response: "Dobrá odpověď", good_response: "Dobrá odpověď",
more_actions: "Další akce", more_actions: "Další akce",
hide_citations: "Skrýt citace",
show_citations: "Zobrazit citace",
fork: "Rozdělit", fork: "Rozdělit",
delete: "Smazat", delete: "Smazat",
save_submit: "Uložit a odeslat",
cancel: "Zrušit", cancel: "Zrušit",
edit_prompt: "Upravit výzvu", edit_prompt: "Upravit výzvu",
edit_response: "Upravit odpověď", 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", preset_reset_description: "Vymazat historii chatu a začít nový chat",
add_new_preset: " Přidat novou předvolbu", add_new_preset: " Přidat novou předvolbu",
command: "Příkaz", command: "Příkaz",
@ -841,6 +819,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Klikněte pro nastavení přihlašovacích údajů", "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: { profile_settings: {
edit_account: "Upravit účet", edit_account: "Upravit účet",

View File

@ -150,12 +150,6 @@ const TRANSLATIONS = {
heading: "Forklar mig", heading: "Forklar mig",
body: "fordelene ved AnythingLLM", body: "fordelene ved AnythingLLM",
}, },
pfp: {
title: "Assistentens profilbillede",
description: "Tilpas assistentens profilbillede for dette arbejdsområde.",
image: "Arbejdsområdebillede",
remove: "Fjern arbejdsområdebillede",
},
delete: { delete: {
title: "Slet arbejdsområde", title: "Slet arbejdsområde",
description: description:
@ -656,8 +650,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Send en besked", send_message: "Send en besked",
attach_file: "Vedhæft en fil til denne chat", 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.", text_size: "Ændr tekststørrelse.",
microphone: "Tal din prompt.", microphone: "Tal din prompt.",
send: "Send promptbesked til arbejdsområdet", send: "Send promptbesked til arbejdsområdet",
@ -669,18 +661,11 @@ const TRANSLATIONS = {
regenerate_response: "Genopbyg svar", regenerate_response: "Genopbyg svar",
good_response: "Godt svar", good_response: "Godt svar",
more_actions: "Flere handlinger", more_actions: "Flere handlinger",
hide_citations: "Skjul henvisninger",
show_citations: "Vis henvisninger",
fork: "Fork", fork: "Fork",
delete: "Slet", delete: "Slet",
save_submit: "Gem og indsende",
cancel: "Annullér", cancel: "Annullér",
edit_prompt: "Redigeringsanmodning", edit_prompt: "Redigeringsanmodning",
edit_response: "Rediger svar", 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: preset_reset_description:
"Rydd op i din chat-historik og start en ny samtale", "Rydd op i din chat-historik og start en ny samtale",
add_new_preset: "Tilføj ny forudindstilling", add_new_preset: "Tilføj ny forudindstilling",
@ -706,6 +691,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Klik for at oprette legitimationsoplysninger", "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: { profile_settings: {
edit_account: "Rediger konto", edit_account: "Rediger konto",
@ -773,10 +788,6 @@ const TRANSLATIONS = {
description: description:
"Angiv et navn, der vises på login-siden for alle brugere.", "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": { "display-language": {
title: "Visningssprog", title: "Visningssprog",
description: description:

View File

@ -157,13 +157,6 @@ const TRANSLATIONS = {
heading: "Erkläre mir", heading: "Erkläre mir",
body: "die Vorteile von AnythingLLM", 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: { delete: {
title: "Workspace löschen", title: "Workspace löschen",
description: description:
@ -392,11 +385,6 @@ const TRANSLATIONS = {
description: description:
"Geben Sie einen Anwendungsnamen ein, der auf der Login-Seite erscheint.", "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": { "display-language": {
title: "Sprache", title: "Sprache",
description: description:
@ -772,8 +760,6 @@ const TRANSLATIONS = {
attachments_processing: "Anhänge werden verarbeitet. Bitte warten...", attachments_processing: "Anhänge werden verarbeitet. Bitte warten...",
send_message: "Schreibe eine Nachricht", send_message: "Schreibe eine Nachricht",
attach_file: "Füge eine Datei zum Chat hinzu", 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.", text_size: "Ändere die Größe des Textes.",
microphone: "Spreche deinen Prompt ein.", microphone: "Spreche deinen Prompt ein.",
send: "Versende den Prompt an den Workspace.", send: "Versende den Prompt an den Workspace.",
@ -783,18 +769,11 @@ const TRANSLATIONS = {
regenerate_response: "Antwort neu generieren", regenerate_response: "Antwort neu generieren",
good_response: "Gute Antwort", good_response: "Gute Antwort",
more_actions: "Weitere Aktionen", more_actions: "Weitere Aktionen",
hide_citations: "Quellenangaben ausblenden",
show_citations: "Quellenangaben anzeigen",
fork: "Abzweigen", fork: "Abzweigen",
delete: "Löschen", delete: "Löschen",
save_submit: "Speichern und Senden",
cancel: "Abbrechen", cancel: "Abbrechen",
edit_prompt: "Prompt bearbeiten", edit_prompt: "Prompt bearbeiten",
edit_response: "Antwort 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", preset_reset_description: "Chatverlauf löschen und neuen Chat starten",
add_new_preset: "Neues Preset anlegen", add_new_preset: "Neues Preset anlegen",
command: "Befehl", command: "Befehl",
@ -817,6 +796,36 @@ const TRANSLATIONS = {
missing_credentials: "Für diesen Anbieter fehlen Anmeldedaten!", missing_credentials: "Für diesen Anbieter fehlen Anmeldedaten!",
missing_credentials_description: "Klicken, um Zugangsdaten einzurichten", 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: { profile_settings: {
edit_account: "Account bearbeiten", edit_account: "Account bearbeiten",

View File

@ -163,13 +163,6 @@ const TRANSLATIONS = {
heading: "Explain to me", heading: "Explain to me",
body: "the benefits of AnythingLLM", 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: { delete: {
title: "Delete Workspace", title: "Delete Workspace",
description: description:
@ -397,11 +390,6 @@ const TRANSLATIONS = {
description: description:
"Set a name that is displayed on the login page to all users.", "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": { "display-language": {
title: "Display Language", title: "Display Language",
description: description:
@ -795,8 +783,6 @@ const TRANSLATIONS = {
attachments_processing: "Attachments are processing. Please wait...", attachments_processing: "Attachments are processing. Please wait...",
send_message: "Send a message", send_message: "Send a message",
attach_file: "Attach a file to this chat", 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.", text_size: "Change text size.",
microphone: "Speak your prompt.", microphone: "Speak your prompt.",
send: "Send prompt message to workspace", send: "Send prompt message to workspace",
@ -806,20 +792,31 @@ const TRANSLATIONS = {
regenerate_response: "Regenerate response", regenerate_response: "Regenerate response",
good_response: "Good response", good_response: "Good response",
more_actions: "More actions", more_actions: "More actions",
hide_citations: "Hide citations", sources: "Sources",
show_citations: "Show citations", 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", fork: "Fork",
delete: "Delete", delete: "Delete",
save_submit: "Save & Submit",
cancel: "Cancel", cancel: "Cancel",
submit: "Submit",
edit_prompt: "Edit prompt", edit_prompt: "Edit prompt",
edit_response: "Edit response", edit_response: "Edit response",
at_agent: "@agent", edit_info_user:
default_agent_description: " - the default agent for this workspace.", '"Submit" regenerates the AI response. "Save" updates your message only.',
custom_agents_coming_soon: "custom agents are coming soon!", edit_info_assistant:
slash_reset: "/reset", "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_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_preset: " Add New Preset",
add_new: "Add new",
edit: "Edit",
publish: "Publish",
stop_generating: "Stop generating response",
command: "Command", command: "Command",
your_command: "your-command", your_command: "your-command",
placeholder_prompt: placeholder_prompt:
@ -830,15 +827,27 @@ const TRANSLATIONS = {
small: "Small", small: "Small",
normal: "Normal", normal: "Normal",
large: "Large", 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: { workspace_llm_manager: {
search: "Search LLM providers", search: "Search",
loading_workspace_settings: "Loading workspace settings...", loading_workspace_settings: "Loading workspace settings...",
available_models: "Available Models for {{provider}}", available_models: "Available Models for {{provider}}",
available_models_description: "Select a model to use for this workspace.", available_models_description: "Select a model to use for this workspace.",
save: "Use this model", save: "Use this model",
saving: "Setting model as workspace default...", saving: "Setting model as workspace default...",
missing_credentials: "This provider is missing credentials!", missing_credentials: "This provider is missing credentials!",
missing_credentials_description: "Click to set up credentials", missing_credentials_description: "Set up now",
}, },
}, },
profile_settings: { profile_settings: {

View File

@ -158,13 +158,6 @@ const TRANSLATIONS = {
heading: "Explícame", heading: "Explícame",
body: "los beneficios de AnythingLLM", 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: { delete: {
title: "Eliminar espacio de trabajo", title: "Eliminar espacio de trabajo",
description: description:
@ -400,11 +393,6 @@ const TRANSLATIONS = {
description: description:
"Establece un nombre que se mostrará en la página de inicio de sesión para todos los usuarios.", "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": { "display-language": {
title: "Idioma de visualización", title: "Idioma de visualización",
description: description:
@ -782,8 +770,6 @@ const TRANSLATIONS = {
"Los archivos adjuntos se están procesando. Por favor, espera...", "Los archivos adjuntos se están procesando. Por favor, espera...",
send_message: "Enviar un mensaje", send_message: "Enviar un mensaje",
attach_file: "Adjuntar un archivo a este chat", 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.", text_size: "Cambiar tamaño del texto.",
microphone: "Habla tu prompt.", microphone: "Habla tu prompt.",
send: "Enviar mensaje de prompt al espacio de trabajo", send: "Enviar mensaje de prompt al espacio de trabajo",
@ -793,19 +779,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerar respuesta", regenerate_response: "Regenerar respuesta",
good_response: "Buena respuesta", good_response: "Buena respuesta",
more_actions: "Más acciones", more_actions: "Más acciones",
hide_citations: "Ocultar citas",
show_citations: "Mostrar citas",
fork: "Bifurcar", fork: "Bifurcar",
delete: "Eliminar", delete: "Eliminar",
save_submit: "Guardar y enviar",
cancel: "Cancelar", cancel: "Cancelar",
edit_prompt: "Editar prompt", edit_prompt: "Editar prompt",
edit_response: "Editar respuesta", 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: preset_reset_description:
"Borra tu historial de chat y comienza un nuevo chat", "Borra tu historial de chat y comienza un nuevo chat",
add_new_preset: " Agregar nuevo preajuste", add_new_preset: " Agregar nuevo preajuste",
@ -833,6 +811,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Haz clic para configurar las credenciales", "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: { profile_settings: {
edit_account: "Editar cuenta", edit_account: "Editar cuenta",

View File

@ -154,12 +154,6 @@ const TRANSLATIONS = {
heading: "Selgita mulle", heading: "Selgita mulle",
body: "AnythingLLM eeliseid", body: "AnythingLLM eeliseid",
}, },
pfp: {
title: "Abilise profiilipilt",
description: "Kohanda selle tööruumi abilise profiilipilti.",
image: "Tööruumi pilt",
remove: "Eemalda tööruumi pilt",
},
delete: { delete: {
title: "Kustuta tööruum", title: "Kustuta tööruum",
description: description:
@ -378,10 +372,6 @@ const TRANSLATIONS = {
description: description:
"Nimi, mis kuvatakse kõigile kasutajatele sisselogimislehel.", "Nimi, mis kuvatakse kõigile kasutajatele sisselogimislehel.",
}, },
"chat-message-alignment": {
title: "Vestlussõnumite joondus",
description: "Vali sõnumite joondus vestlusliideses.",
},
"display-language": { "display-language": {
title: "Kuvakeel", title: "Kuvakeel",
description: description:
@ -738,8 +728,6 @@ const TRANSLATIONS = {
attachments_processing: "Manused töötlevad. Palun oota…", attachments_processing: "Manused töötlevad. Palun oota…",
send_message: "Saada sõnum", send_message: "Saada sõnum",
attach_file: "Lisa fail vestlusele", 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.", text_size: "Muuda teksti suurust.",
microphone: "Esita päring häälega.", microphone: "Esita päring häälega.",
send: "Saada päring tööruumi", send: "Saada päring tööruumi",
@ -749,18 +737,11 @@ const TRANSLATIONS = {
regenerate_response: "Loo vastus uuesti", regenerate_response: "Loo vastus uuesti",
good_response: "Hea vastus", good_response: "Hea vastus",
more_actions: "Rohkem toiminguid", more_actions: "Rohkem toiminguid",
hide_citations: "Peida viited",
show_citations: "Näita viiteid",
fork: "Hargnemine", fork: "Hargnemine",
delete: "Kustuta", delete: "Kustuta",
save_submit: "Salvesta ja saada",
cancel: "Tühista", cancel: "Tühista",
edit_prompt: "Redigeeri päringut", edit_prompt: "Redigeeri päringut",
edit_response: "Redigeeri vastust", 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", preset_reset_description: "Tühjenda vestlusajalugu ja alusta uut vestlust",
add_new_preset: " Lisa uus preset", add_new_preset: " Lisa uus preset",
command: "Käsk", command: "Käsk",
@ -782,6 +763,35 @@ const TRANSLATIONS = {
missing_credentials: "Sellel pakkujal puuduvad võtmed!", missing_credentials: "Sellel pakkujal puuduvad võtmed!",
missing_credentials_description: "Klõpsa, et määrata 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: { profile_settings: {
edit_account: "Muuda kontot", edit_account: "Muuda kontot",

View File

@ -149,12 +149,6 @@ const TRANSLATIONS = {
heading: "برایم توضیح بده", heading: "برایم توضیح بده",
body: "مزایای AnythingLLM را", body: "مزایای AnythingLLM را",
}, },
pfp: {
title: "تصویر پروفایل دستیار",
description: "تصویر پروفایل دستیار را برای این فضای کاری شخصی‌سازی کنید.",
image: "تصویر فضای کاری",
remove: "حذف تصویر فضای کاری",
},
delete: { delete: {
title: "حذف فضای کاری", title: "حذف فضای کاری",
description: description:
@ -654,9 +648,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "یک پیام ارسال کنید", send_message: "یک پیام ارسال کنید",
attach_file: "لطفاً یک فایل را به این چت پیوست کنید.", attach_file: "لطفاً یک فایل را به این چت پیوست کنید.",
slash: "برای مشاهده تمام دستورات Slash موجود برای چت.",
agents:
"تمام عوامل موجود را که می‌توانید برای گفتگو استفاده کنید، مشاهده کنید.",
text_size: "تغییر اندازه متن.", text_size: "تغییر اندازه متن.",
microphone: "سوال خود را بپرسید.", microphone: "سوال خود را بپرسید.",
send: "پیام فوری را برای فضای کاری ارسال کنید", send: "پیام فوری را برای فضای کاری ارسال کنید",
@ -667,18 +658,11 @@ const TRANSLATIONS = {
regenerate_response: "بازسازی پاسخ", regenerate_response: "بازسازی پاسخ",
good_response: "پاسخ خوب", good_response: "پاسخ خوب",
more_actions: "اقدامات بیشتر", more_actions: "اقدامات بیشتر",
hide_citations: "پنهان کردن ارجاعات",
show_citations: "نمایش ارجاعات",
fork: "چنگال", fork: "چنگال",
delete: "حذف", delete: "حذف",
save_submit: "ذخیره و ارسال",
cancel: "ยกد", cancel: "ยกد",
edit_prompt: "لطفاً دستور ویرایش را ارائه دهید.", edit_prompt: "لطفاً دستور ویرایش را ارائه دهید.",
edit_response: "لطفا پاسخ را ویرایش کنید.", edit_response: "لطفا پاسخ را ویرایش کنید.",
at_agent: "@agent",
default_agent_description: "- عامل پیش‌فرض برای این فضای کاری.",
custom_agents_coming_soon: "نمایندگان ویژه در حال آمدن هستند!",
slash_reset: "/reset",
preset_reset_description: "حذف تاریخچه چت خود و شروع یک چت جدید", preset_reset_description: "حذف تاریخچه چت خود و شروع یک چت جدید",
add_new_preset: "اضافه کردن تنظیمات پیش‌فرض جدید", add_new_preset: "اضافه کردن تنظیمات پیش‌فرض جدید",
command: "دستورالعمل", command: "دستورالعمل",
@ -703,6 +687,35 @@ const TRANSLATIONS = {
missing_credentials_description: 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: { profile_settings: {
edit_account: "ویرایش حساب", edit_account: "ویرایش حساب",
@ -768,11 +781,6 @@ const TRANSLATIONS = {
title: "نام", title: "نام",
description: "یک نام را برای تمام کاربران در صفحه ورود مشخص کنید.", description: "یک نام را برای تمام کاربران در صفحه ورود مشخص کنید.",
}, },
"chat-message-alignment": {
title: "همراه‌بودن پیام‌ها در چت",
description:
"هنگام استفاده از رابط چت، حالت هم‌تراز کردن پیام را انتخاب کنید.",
},
"display-language": { "display-language": {
title: "زبان نمایش", title: "زبان نمایش",
description: description:

View File

@ -150,13 +150,6 @@ const TRANSLATIONS = {
heading: "Expliquez-moi", heading: "Expliquez-moi",
body: "les avantages de AnythingLLM", 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: { delete: {
title: "Supprimer l'Espace de Travail", title: "Supprimer l'Espace de Travail",
description: description:
@ -656,8 +649,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Envoyer un message", send_message: "Envoyer un message",
attach_file: "Joindre un fichier", attach_file: "Joindre un fichier",
slash: "Voir les commandes slash disponibles",
agents: "Voir les agents disponibles",
text_size: "Modifier la taille du texte", text_size: "Modifier la taille du texte",
microphone: "Enregistrer un message vocal", microphone: "Enregistrer un message vocal",
send: "Envoyer le message au chatbot", send: "Envoyer le message au chatbot",
@ -669,18 +660,11 @@ const TRANSLATIONS = {
regenerate_response: "Régénérer la réponse", regenerate_response: "Régénérer la réponse",
good_response: "Bonne réponse", good_response: "Bonne réponse",
more_actions: "Plus d'actions", more_actions: "Plus d'actions",
hide_citations: "Masquer les citations",
show_citations: "Afficher les citations",
fork: "Dupliquer", fork: "Dupliquer",
delete: "Supprimer", delete: "Supprimer",
save_submit: "Sauvegarder et envoyer",
cancel: "Annuler", cancel: "Annuler",
edit_prompt: "Modifier le prompt", edit_prompt: "Modifier le prompt",
edit_response: "Modifier la réponse", 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: preset_reset_description:
"Efface l'historique du chat actuel et commence une nouvelle conversation.", "Efface l'historique du chat actuel et commence une nouvelle conversation.",
add_new_preset: "Ajouter une nouvelle commande preset", add_new_preset: "Ajouter une nouvelle commande preset",
@ -706,6 +690,37 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Vous devez configurer vos identifiants de fournisseur LLM avant de pouvoir sélectionner un modèle.", "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: { profile_settings: {
edit_account: "Modifier le compte", edit_account: "Modifier le compte",
@ -773,10 +788,6 @@ const TRANSLATIONS = {
title: "Nom de l'application", title: "Nom de l'application",
description: "Définissez le nom affiché dans l'interface.", 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": { "display-language": {
title: "Langue d'affichage", title: "Langue d'affichage",
description: "Sélectionnez la langue de l'interface utilisateur.", description: "Sélectionnez la langue de l'interface utilisateur.",

View File

@ -152,12 +152,6 @@ const TRANSLATIONS = {
heading: "הסבר לי", heading: "הסבר לי",
body: "את היתרונות של AnythingLLM", body: "את היתרונות של AnythingLLM",
}, },
pfp: {
title: "תמונת פרופיל של העוזר",
description: "התאם אישית את תמונת הפרופיל של העוזר עבור סביבת עבודה זו.",
image: "תמונת סביבת עבודה",
remove: "הסר תמונת סביבת עבודה",
},
delete: { delete: {
title: "מחק סביבת עבודה", title: "מחק סביבת עבודה",
description: description:
@ -379,10 +373,6 @@ const TRANSLATIONS = {
title: "שם", title: "שם",
description: "הגדר שם שיוצג בדף ההתחברות לכל המשתמשים.", description: "הגדר שם שיוצג בדף ההתחברות לכל המשתמשים.",
}, },
"chat-message-alignment": {
title: "יישור הודעות צ'אט",
description: "בחר את מצב יישור ההודעות בעת שימוש בממשק הצ'אט.",
},
"display-language": { "display-language": {
title: "שפת תצוגה", title: "שפת תצוגה",
description: description:
@ -742,8 +732,6 @@ const TRANSLATIONS = {
attachments_processing: "קבצים מצורפים בעיבוד. אנא המתן...", attachments_processing: "קבצים מצורפים בעיבוד. אנא המתן...",
send_message: "שלח הודעה", send_message: "שלח הודעה",
attach_file: "צרף קובץ לצ'אט זה", attach_file: "צרף קובץ לצ'אט זה",
slash: "הצג את כל פקודות הסלאש הזמינות לצ'אט.",
agents: "הצג את כל הסוכנים הזמינים שתוכל להשתמש בהם לצ'אט.",
text_size: "שנה גודל טקסט.", text_size: "שנה גודל טקסט.",
microphone: "אמור את ההנחיה שלך.", microphone: "אמור את ההנחיה שלך.",
send: "שלח הודעת הנחיה לסביבת העבודה", send: "שלח הודעת הנחיה לסביבת העבודה",
@ -753,18 +741,11 @@ const TRANSLATIONS = {
regenerate_response: "צור תגובה מחדש", regenerate_response: "צור תגובה מחדש",
good_response: "תגובה טובה", good_response: "תגובה טובה",
more_actions: "פעולות נוספות", more_actions: "פעולות נוספות",
hide_citations: "הסתר ציטוטים",
show_citations: "הצג ציטוטים",
fork: "פצל (Fork)", fork: "פצל (Fork)",
delete: "מחק", delete: "מחק",
save_submit: "שמור ושלח",
cancel: "בטל", cancel: "בטל",
edit_prompt: "ערוך הנחיה", edit_prompt: "ערוך הנחיה",
edit_response: "ערוך תגובה", edit_response: "ערוך תגובה",
at_agent: "@agent",
default_agent_description: " - סוכן ברירת המחדל עבור סביבת עבודה זו.",
custom_agents_coming_soon: "סוכנים מותאמים אישית יגיעו בקרוב!",
slash_reset: "/reset",
preset_reset_description: "נקה את היסטוריית הצ'אט שלך והתחל צ'אט חדש", preset_reset_description: "נקה את היסטוריית הצ'אט שלך והתחל צ'אט חדש",
add_new_preset: " הוסף הגדרה קבועה חדשה", add_new_preset: " הוסף הגדרה קבועה חדשה",
command: "פקודה", command: "פקודה",
@ -786,6 +767,36 @@ const TRANSLATIONS = {
missing_credentials: "חסרים אישורים לספק זה!", missing_credentials: "חסרים אישורים לספק זה!",
missing_credentials_description: "לחץ להגדרת אישורים", 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: { profile_settings: {
edit_account: "ערוך חשבון", edit_account: "ערוך חשבון",

View File

@ -151,13 +151,6 @@ const TRANSLATIONS = {
heading: "Spiegami", heading: "Spiegami",
body: "i vantaggi di AnythingLLM", 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: { delete: {
title: "Elimina area di lavoro", title: "Elimina area di lavoro",
description: description:
@ -660,9 +653,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Invia un messaggio", send_message: "Invia un messaggio",
attach_file: "Allega un file a questa chat.", 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.", text_size: "Modifica la dimensione del testo.",
microphone: "Formula la tua richiesta.", microphone: "Formula la tua richiesta.",
send: "Invia un messaggio immediato allo spazio di lavoro", 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.", "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.", good_response: "Ottima risposta.",
more_actions: "Ulteriori azioni", more_actions: "Ulteriori azioni",
hide_citations: "Nascondi le citazioni",
show_citations: "Mostra citazioni",
fork: "Forchetta", fork: "Forchetta",
delete: "Elimina", delete: "Elimina",
save_submit: "Salva e invia",
cancel: "Annulla", cancel: "Annulla",
edit_prompt: "Suggerimento di modifica:", edit_prompt: "Suggerimento di modifica:",
edit_response: "Modifica la risposta", 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: preset_reset_description:
"Elimina la cronologia delle chat e avvia una nuova chat", "Elimina la cronologia delle chat e avvia una nuova chat",
add_new_preset: "Aggiungi nuovo preset", add_new_preset: "Aggiungi nuovo preset",
@ -716,6 +698,37 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Fare clic per configurare le credenziali", "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: { profile_settings: {
edit_account: "Modifica account", edit_account: "Modifica account",
@ -788,11 +801,6 @@ const TRANSLATIONS = {
description: description:
"Definisci un nome che verrà visualizzato sulla pagina di accesso per tutti gli utenti.", "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": { "display-language": {
title: "Lingua da visualizzare", title: "Lingua da visualizzare",
description: description:

View File

@ -148,13 +148,6 @@ const TRANSLATIONS = {
heading: "説明してください", heading: "説明してください",
body: "AnythingLLMの利点", body: "AnythingLLMの利点",
}, },
pfp: {
title: "アシスタントのプロフィール画像",
description:
"このワークスペースのアシスタントのプロフィール画像をカスタマイズします。",
image: "ワークスペース画像",
remove: "ワークスペース画像を削除",
},
delete: { delete: {
title: "ワークスペースを削除", title: "ワークスペースを削除",
description: description:
@ -646,8 +639,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "メッセージを送信", send_message: "メッセージを送信",
attach_file: "このチャットにファイルを添付", attach_file: "このチャットにファイルを添付",
slash: "チャットで使えるスラッシュコマンドをすべて表示",
agents: "利用可能なエージェントをすべて表示",
text_size: "テキストサイズを変更", text_size: "テキストサイズを変更",
microphone: "プロンプトを音声入力", microphone: "プロンプトを音声入力",
send: "ワークスペースにプロンプトメッセージを送信", send: "ワークスペースにプロンプトメッセージを送信",
@ -660,18 +651,11 @@ const TRANSLATIONS = {
good_response: "良い反応", good_response: "良い反応",
more_actions: more_actions:
"さらに詳細な情報が必要な場合は、お気軽にお問い合わせください。", "さらに詳細な情報が必要な場合は、お気軽にお問い合わせください。",
hide_citations: "参考文献を隠す",
show_citations: "引用元を表示する",
fork: "フォーク", fork: "フォーク",
delete: "削除", delete: "削除",
save_submit: "保存して送信",
cancel: "キャンセル", cancel: "キャンセル",
edit_prompt: "編集のヒント", edit_prompt: "編集のヒント",
edit_response: "編集内容を保存します。", edit_response: "編集内容を保存します。",
at_agent: "@agent",
default_agent_description: "- このワークスペースのデフォルトエージェント。",
custom_agents_coming_soon: "カスタムエージェントは近日公開予定です。",
slash_reset: "/reset",
preset_reset_description: preset_reset_description:
"チャット履歴をクリアし、新しいチャットを開始してください。", "チャット履歴をクリアし、新しいチャットを開始してください。",
add_new_preset: "新しいプリセットを追加する", add_new_preset: "新しいプリセットを追加する",
@ -696,6 +680,35 @@ const TRANSLATIONS = {
missing_credentials_description: 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: { profile_settings: {
edit_account: "アカウントを編集", edit_account: "アカウントを編集",
@ -764,11 +777,6 @@ const TRANSLATIONS = {
description: description:
"ログインページに表示される名前を、すべてのユーザーに設定する。", "ログインページに表示される名前を、すべてのユーザーに設定する。",
}, },
"chat-message-alignment": {
title: "チャットメッセージの整合性を確認する",
description:
"チャットインターフェースを使用する場合、メッセージの配置モードを選択してください。",
},
"display-language": { "display-language": {
title: "表示言語", title: "表示言語",
description: description:

View File

@ -153,12 +153,6 @@ const TRANSLATIONS = {
heading: "저에게 설명해주세요", heading: "저에게 설명해주세요",
body: "AnythingLLM의 장점", body: "AnythingLLM의 장점",
}, },
pfp: {
title: "어시스턴트 프로필 이미지",
description: "이 워크스페이스의 어시스턴트 프로필 이미지를 수정합니다.",
image: "워크스페이스 이미지",
remove: "워크스페이스 이미지 제거",
},
delete: { delete: {
title: "워크스페이스 삭제", title: "워크스페이스 삭제",
description: description:
@ -383,10 +377,6 @@ const TRANSLATIONS = {
description: description:
"로그인 페이지에 모든 사용자에게 표시될 애플리케이션 이름을 설정하세요.", "로그인 페이지에 모든 사용자에게 표시될 애플리케이션 이름을 설정하세요.",
}, },
"chat-message-alignment": {
title: "채팅 메시지 정렬",
description: "채팅 인터페이스에서 메시지 정렬 방식을 선택하세요.",
},
"display-language": { "display-language": {
title: "표시 언어", title: "표시 언어",
description: description:
@ -751,8 +741,6 @@ const TRANSLATIONS = {
"첨부 파일을 처리 중입니다. 잠시만 기다려 주세요...", "첨부 파일을 처리 중입니다. 잠시만 기다려 주세요...",
send_message: "메시지 보내기", send_message: "메시지 보내기",
attach_file: "이 채팅에 파일 첨부", attach_file: "이 채팅에 파일 첨부",
slash: "채팅에서 사용할 수 있는 모든 슬래시 명령어 보기",
agents: "채팅에 사용할 수 있는 모든 에이전트 보기",
text_size: "텍스트 크기 변경", text_size: "텍스트 크기 변경",
microphone: "프롬프트를 음성으로 입력", microphone: "프롬프트를 음성으로 입력",
send: "프롬프트 메시지를 워크스페이스로 전송", send: "프롬프트 메시지를 워크스페이스로 전송",
@ -762,18 +750,11 @@ const TRANSLATIONS = {
regenerate_response: "응답 다시 생성", regenerate_response: "응답 다시 생성",
good_response: "좋은 답변", good_response: "좋은 답변",
more_actions: "더 많은 작업", more_actions: "더 많은 작업",
hide_citations: "인용 숨기기",
show_citations: "인용 보기",
fork: "포크", fork: "포크",
delete: "삭제", delete: "삭제",
save_submit: "저장 및 제출",
cancel: "취소", cancel: "취소",
edit_prompt: "프롬프트 수정", edit_prompt: "프롬프트 수정",
edit_response: "응답 수정", edit_response: "응답 수정",
at_agent: "@agent",
default_agent_description: " - 이 워크스페이스의 기본 에이전트입니다.",
custom_agents_coming_soon: "커스텀 에이전트 기능이 곧 제공됩니다!",
slash_reset: "/reset",
preset_reset_description: "채팅 기록을 초기화하고 새 채팅을 시작합니다", preset_reset_description: "채팅 기록을 초기화하고 새 채팅을 시작합니다",
add_new_preset: "새 프리셋 추가", add_new_preset: "새 프리셋 추가",
command: "명령어", command: "명령어",
@ -796,6 +777,35 @@ const TRANSLATIONS = {
missing_credentials: "이 제공자의 인증 정보가 없습니다!", missing_credentials: "이 제공자의 인증 정보가 없습니다!",
missing_credentials_description: "클릭하여 인증 정보를 설정하세요", 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: { profile_settings: {
edit_account: "계정 정보 수정", edit_account: "계정 정보 수정",

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Izskaidro man", heading: "Izskaidro man",
body: "AnythingLLM priekšrocības", 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: { delete: {
title: "Dzēst darba telpu", title: "Dzēst darba telpu",
description: description:
@ -388,11 +382,6 @@ const TRANSLATIONS = {
description: description:
"Iestatiet nosaukumu, kas tiek rādīts pieteikšanās lapā visiem lietotājiem.", "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": { "display-language": {
title: "Displeja valoda", title: "Displeja valoda",
description: description:
@ -765,8 +754,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Sūtīt ziņojumu", send_message: "Sūtīt ziņojumu",
attach_file: "Pievienot failu šim čatam", 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.", text_size: "Mainīt teksta izmēru.",
microphone: "Izrunājiet savu uzvedni.", microphone: "Izrunājiet savu uzvedni.",
send: "Nosūtīt uzvednes ziņojumu uz darba vietu", send: "Nosūtīt uzvednes ziņojumu uz darba vietu",
@ -777,19 +764,11 @@ const TRANSLATIONS = {
regenerate_response: "Atjaunot atbildi", regenerate_response: "Atjaunot atbildi",
good_response: "Laba atbilde", good_response: "Laba atbilde",
more_actions: "Vairāk darbību", more_actions: "Vairāk darbību",
hide_citations: "Izvākt atsaukmes",
show_citations: "Rādīt atsauces",
fork: "Klūtis", fork: "Klūtis",
delete: "Dzēst", delete: "Dzēst",
save_submit: "Saglabāt un iesūt",
cancel: "Atcelt", cancel: "Atcelt",
edit_prompt: "Ieslēgt", edit_prompt: "Ieslēgt",
edit_response: "Rediģēt atbildi", 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: preset_reset_description:
"Izdzēsiet savu pastā veidoتو sarunu vēsturi un sāciet jaunu sarunu.", "Izdzēsiet savu pastā veidoتو sarunu vēsturi un sāciet jaunu sarunu.",
add_new_preset: "Pievienot jaunu iepriekšējo", add_new_preset: "Pievienot jaunu iepriekšējo",
@ -816,6 +795,37 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Noklikšķiniet, lai konfigurētu autentifikācijas datus", "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: { profile_settings: {
edit_account: "Rediģēt kontu", edit_account: "Rediģēt kontu",

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Leg me uit", heading: "Leg me uit",
body: "de voordelen van AnythingLLM", 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: { delete: {
title: "Werkruimte Verwijderen", title: "Werkruimte Verwijderen",
description: description:
@ -656,9 +649,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Een bericht verzenden", send_message: "Een bericht verzenden",
attach_file: "Een bestand aan deze chat toevoegen", 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.", text_size: "Tekstgrootte wijzigen.",
microphone: "Spreek je prompt uit.", microphone: "Spreek je prompt uit.",
send: "Promptbericht naar werkruimte verzenden", send: "Promptbericht naar werkruimte verzenden",
@ -670,18 +660,11 @@ const TRANSLATIONS = {
regenerate_response: "Reactie opnieuw genereren", regenerate_response: "Reactie opnieuw genereren",
good_response: "Goede reactie", good_response: "Goede reactie",
more_actions: "Meer acties", more_actions: "Meer acties",
hide_citations: "Citaten verbergen",
show_citations: "Citaten weergeven",
fork: "Fork", fork: "Fork",
delete: "Verwijderen", delete: "Verwijderen",
save_submit: "Opslaan en verzenden",
cancel: "Annuleren", cancel: "Annuleren",
edit_prompt: "Prompt bewerken", edit_prompt: "Prompt bewerken",
edit_response: "Reactie 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: preset_reset_description:
"Wis je chatgeschiedenis en begin een nieuwe chat", "Wis je chatgeschiedenis en begin een nieuwe chat",
add_new_preset: "Nieuwe preset toevoegen", add_new_preset: "Nieuwe preset toevoegen",
@ -704,6 +687,36 @@ const TRANSLATIONS = {
missing_credentials: "Deze aanbieder mist logingegevens!", missing_credentials: "Deze aanbieder mist logingegevens!",
missing_credentials_description: "Klik om logingegevens in te stellen", 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: { profile_settings: {
edit_account: "Account bewerken", edit_account: "Account bewerken",
@ -772,11 +785,6 @@ const TRANSLATIONS = {
description: description:
"Stel een naam in die op de inlogpagina voor alle gebruikers wordt weergegeven.", "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": { "display-language": {
title: "Weergavetaal", title: "Weergavetaal",
description: description:

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Wyjaśnij mi", heading: "Wyjaśnij mi",
body: "Korzyści z AnythingLLM", 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: { delete: {
title: "Usuń obszar roboczy", title: "Usuń obszar roboczy",
description: description:
@ -390,11 +384,6 @@ const TRANSLATIONS = {
description: description:
"Ustawienie nazwy wyświetlanej na stronie logowania dla wszystkich użytkowników.", "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": { "display-language": {
title: "Język", title: "Język",
description: description:
@ -767,8 +756,6 @@ const TRANSLATIONS = {
attachments_processing: "Załączniki są przetwarzane. Proszę czekać...", attachments_processing: "Załączniki są przetwarzane. Proszę czekać...",
send_message: "Wyślij wiadomość", send_message: "Wyślij wiadomość",
attach_file: "Dołącz plik do tego czatu", 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.", text_size: "Zmiana rozmiaru tekstu.",
microphone: "Wypowiedz swoją prośbę.", microphone: "Wypowiedz swoją prośbę.",
send: "Wyślij wiadomość do obszaru roboczego", send: "Wyślij wiadomość do obszaru roboczego",
@ -778,18 +765,11 @@ const TRANSLATIONS = {
regenerate_response: "Wygeneruj ponownie odpowiedź", regenerate_response: "Wygeneruj ponownie odpowiedź",
good_response: "Dobra odpowiedź", good_response: "Dobra odpowiedź",
more_actions: "Więcej działań", more_actions: "Więcej działań",
hide_citations: "Ukryj cytaty",
show_citations: "Pokaż cytaty",
fork: "Utwórz rozgałęzienie", fork: "Utwórz rozgałęzienie",
delete: "Usuń", delete: "Usuń",
save_submit: "Zapisz i prześlij",
cancel: "Anuluj", cancel: "Anuluj",
edit_prompt: "Edytuj prompt", edit_prompt: "Edytuj prompt",
edit_response: "Edytuj odpowiedź", 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", preset_reset_description: "Wyczyść historię czatu i rozpocznij nowy czat",
add_new_preset: " Dodaj nowe polecenie slash", add_new_preset: " Dodaj nowe polecenie slash",
command: "Polecenie", command: "Polecenie",
@ -813,6 +793,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Kliknij, aby skonfigurować poświadczenia", "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: { profile_settings: {
edit_account: "Edytuj konto", edit_account: "Edytuj konto",

View File

@ -156,12 +156,6 @@ const TRANSLATIONS = {
heading: "Explique para mim", heading: "Explique para mim",
body: "os benefícios do AnythingLLM", 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: { delete: {
title: "Excluir Workspace", title: "Excluir Workspace",
description: description:
@ -384,10 +378,6 @@ const TRANSLATIONS = {
description: description:
"Defina um nome exibido na página de login para todos os usuários.", "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": { "display-language": {
title: "Idioma", title: "Idioma",
description: description:
@ -748,8 +738,6 @@ const TRANSLATIONS = {
attachments_processing: "Anexos em processamento. Aguarde...", attachments_processing: "Anexos em processamento. Aguarde...",
send_message: "Enviar mensagem", send_message: "Enviar mensagem",
attach_file: "Anexar arquivo ao chat", 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.", text_size: "Alterar tamanho do texto.",
microphone: "Fale seu prompt.", microphone: "Fale seu prompt.",
send: "Enviar prompt para o workspace", send: "Enviar prompt para o workspace",
@ -759,18 +747,11 @@ const TRANSLATIONS = {
regenerate_response: "Regerar resposta", regenerate_response: "Regerar resposta",
good_response: "Resposta satisfatória", good_response: "Resposta satisfatória",
more_actions: "Mais ações", more_actions: "Mais ações",
hide_citations: "Esconder citações",
show_citations: "Exibir citações",
fork: "Fork", fork: "Fork",
delete: "Excluir", delete: "Excluir",
save_submit: "Alterar",
cancel: "Cancelar", cancel: "Cancelar",
edit_prompt: "Editar prompt", edit_prompt: "Editar prompt",
edit_response: "Editar resposta", 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", preset_reset_description: "Limpa o histórico do seu chat e inicia um novo",
add_new_preset: " Insere um novo Preset", add_new_preset: " Insere um novo Preset",
command: "Comando", command: "Comando",
@ -794,6 +775,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Configure as credenciais do LLM primeiro", "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: { profile_settings: {
edit_account: "Editar conta", edit_account: "Editar conta",

View File

@ -158,13 +158,6 @@ const TRANSLATIONS = {
heading: "Explică-mi", heading: "Explică-mi",
body: "beneficiile AnythingLLM", 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: { delete: {
title: "Șterge spațiul de lucru", title: "Șterge spațiul de lucru",
description: description:
@ -498,8 +491,6 @@ const TRANSLATIONS = {
"Fișierele atașate se procesează. Te rugăm să aștepți...", "Fișierele atașate se procesează. Te rugăm să aștepți...",
send_message: "Trimite mesaj", send_message: "Trimite mesaj",
attach_file: "Atașează un fișier la acest chat", 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.", text_size: "Schimbă dimensiunea textului.",
microphone: "Vorbește promptul tău.", microphone: "Vorbește promptul tău.",
send: "Trimite prompt către spațiul de lucru", send: "Trimite prompt către spațiul de lucru",
@ -509,19 +500,11 @@ const TRANSLATIONS = {
regenerate_response: "Regenerare răspuns", regenerate_response: "Regenerare răspuns",
good_response: "Răspuns bun", good_response: "Răspuns bun",
more_actions: "Mai multe acțiuni", more_actions: "Mai multe acțiuni",
hide_citations: "Ascunde citările",
show_citations: "Arată citările",
fork: "Fork", fork: "Fork",
delete: "Șterge", delete: "Șterge",
save_submit: "Salvează & Trimite",
cancel: "Anulează", cancel: "Anulează",
edit_prompt: "Editează prompt", edit_prompt: "Editează prompt",
edit_response: "Editează răspuns", 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: preset_reset_description:
"Șterge istoricul chatului și începe o conversație nouă", "Șterge istoricul chatului și începe o conversație nouă",
add_new_preset: " Adaugă preset nou", add_new_preset: " Adaugă preset nou",
@ -546,6 +529,37 @@ const TRANSLATIONS = {
missing_credentials: "Acest furnizor lipsește credențiale!", missing_credentials: "Acest furnizor lipsește credențiale!",
missing_credentials_description: "Click pentru a configura credențialele", 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: { profile_settings: {
edit_account: "Editează contul", edit_account: "Editează contul",
@ -815,11 +829,6 @@ const TRANSLATIONS = {
description: description:
"Setează un nume care este afișat pe pagina de autentificare tuturor utilizatorilor.", "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": { "display-language": {
title: "Limba de afișare", title: "Limba de afișare",
description: description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Объясните мне", heading: "Объясните мне",
body: "преимущества AnythingLLM", body: "преимущества AnythingLLM",
}, },
pfp: {
title: "Изображение профиля помощника",
description:
"Настройте изображение профиля помощника для этого рабочего пространства.",
image: "Изображение рабочего пространства",
remove: "Удалить изображение рабочего пространства",
},
delete: { delete: {
title: "Удалить Рабочее Пространство", title: "Удалить Рабочее Пространство",
description: description:
@ -654,8 +647,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Отправить сообщение", send_message: "Отправить сообщение",
attach_file: "Прикрепить файл к чату", attach_file: "Прикрепить файл к чату",
slash: "Просмотреть все доступные слэш-команды для чата.",
agents: "Просмотреть всех доступных агентов для чата.",
text_size: "Изменить размер текста.", text_size: "Изменить размер текста.",
microphone: "Произнесите ваш запрос.", microphone: "Произнесите ваш запрос.",
send: "Отправить запрос в рабочее пространство", send: "Отправить запрос в рабочее пространство",
@ -666,20 +657,12 @@ const TRANSLATIONS = {
regenerate_response: "Перефразировать ответ", regenerate_response: "Перефразировать ответ",
good_response: "Хороший ответ", good_response: "Хороший ответ",
more_actions: "Больше действий", more_actions: "Больше действий",
hide_citations: "Скрыть ссылки на источники",
show_citations: "Отображение ссылок",
fork: "Вилка", fork: "Вилка",
delete: "Удалить", delete: "Удалить",
save_submit: "Сохранить и отправить",
cancel: "Отменить", cancel: "Отменить",
edit_prompt: edit_prompt:
"Пожалуйста, предоставьте текст, который необходимо отредактировать.", "Пожалуйста, предоставьте текст, который необходимо отредактировать.",
edit_response: "Отредактируйте ответ", edit_response: "Отредактируйте ответ",
at_agent: "@agent",
default_agent_description:
"- это основной агент для данного рабочего пространства.",
custom_agents_coming_soon: "Скоро появятся индивидуальные агенты!",
slash_reset: "/reset",
preset_reset_description: "Очистите историю чата и начните новый чат", preset_reset_description: "Очистите историю чата и начните новый чат",
add_new_preset: "Добавить новый шаблон", add_new_preset: "Добавить новый шаблон",
command: "Команда", command: "Команда",
@ -707,6 +690,37 @@ const TRANSLATIONS = {
missing_credentials_description: 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: { profile_settings: {
edit_account: "Редактировать учётную запись", edit_account: "Редактировать учётную запись",
@ -777,11 +791,6 @@ const TRANSLATIONS = {
description: description:
"Укажите имя, которое будет отображаться на странице входа для всех пользователей.", "Укажите имя, которое будет отображаться на странице входа для всех пользователей.",
}, },
"chat-message-alignment": {
title: "Выравнивание сообщений в чате",
description:
"Выберите режим выравнивания сообщений при использовании интерфейса чата.",
},
"display-language": { "display-language": {
title: "Язык отображения", title: "Язык отображения",
description: description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Bana açıkla", heading: "Bana açıkla",
body: "AnythingLLM'nin faydalarını", 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: { delete: {
title: "Çalışma Alanını Sil", title: "Çalışma Alanını Sil",
description: description:
@ -653,8 +646,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Mesaj gönderin", send_message: "Mesaj gönderin",
attach_file: "Bu sohbete bir dosya ekleyin", 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.", text_size: "Metin boyutunu değiştirin.",
microphone: "Promptunuzu söyleyin.", microphone: "Promptunuzu söyleyin.",
send: "Çalışma alanına prompt mesajı gönderin", send: "Çalışma alanına prompt mesajı gönderin",
@ -665,18 +656,11 @@ const TRANSLATIONS = {
regenerate_response: "Yanıtı yeniden oluştur", regenerate_response: "Yanıtı yeniden oluştur",
good_response: "İyi yanıt", good_response: "İyi yanıt",
more_actions: "Daha fazla eylem", more_actions: "Daha fazla eylem",
hide_citations: "Alıntıları gizle",
show_citations: "Alıntıları göster",
fork: "Çatalla", fork: "Çatalla",
delete: "Sil", delete: "Sil",
save_submit: "Kaydet & Gönder",
cancel: "İptal", cancel: "İptal",
edit_prompt: "Promptu düzenle", edit_prompt: "Promptu düzenle",
edit_response: "Yanıtı 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: preset_reset_description:
"Sohbet geçmişinizi temizleyin ve yeni bir sohbet başlatın", "Sohbet geçmişinizi temizleyin ve yeni bir sohbet başlatın",
add_new_preset: " Yeni Ön Ayar Ekle", add_new_preset: " Yeni Ön Ayar Ekle",
@ -701,6 +685,36 @@ const TRANSLATIONS = {
missing_credentials_description: missing_credentials_description:
"Kimlik bilgilerini ayarlamak için tıklayın", "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: { profile_settings: {
edit_account: "Hesabı Düzenle", edit_account: "Hesabı Düzenle",
@ -770,11 +784,6 @@ const TRANSLATIONS = {
description: description:
"Giriş sayfasında tüm kullanıcılara gösterilen bir ad ayarlayın.", "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": { "display-language": {
title: "Görüntüleme Dili", title: "Görüntüleme Dili",
description: description:

View File

@ -149,13 +149,6 @@ const TRANSLATIONS = {
heading: "Giải thích cho tôi", heading: "Giải thích cho tôi",
body: "các lợi ích của AnythingLLM", 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: { delete: {
title: "Xóa không gian làm việc", title: "Xóa không gian làm việc",
description: description:
@ -651,8 +644,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "Gửi tin nhắn", send_message: "Gửi tin nhắn",
attach_file: "Đính kèm tệp vào cuộc trò chuyện này", 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.", text_size: "Thay đổi kích thước văn bản.",
microphone: "Nói prompt của 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", 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", regenerate_response: "Tạo lại phản hồi",
good_response: "Phản hồi tốt", good_response: "Phản hồi tốt",
more_actions: "Thêm hành động", 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", fork: "Rẽ nhánh",
delete: "Xóa", delete: "Xóa",
save_submit: "Lưu & Gửi",
cancel: "Hủy", cancel: "Hủy",
edit_prompt: "Chỉnh sửa prompt", edit_prompt: "Chỉnh sửa prompt",
edit_response: "Chỉnh sửa phản hồi", 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: preset_reset_description:
"Xóa lịch sử trò chuyện và bắt đầu cuộc trò chuyện mới", "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", 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: "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", 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: { profile_settings: {
edit_account: "Chỉnh sửa Tài khoản", edit_account: "Chỉnh sửa Tài khoản",
@ -766,11 +780,6 @@ const TRANSLATIONS = {
description: description:
"Đặt tên được hiển thị trên trang đăng nhập cho tất cả người dùng.", "Đặ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": { "display-language": {
title: "Ngôn ngữ Hiển thị", title: "Ngôn ngữ Hiển thị",
description: description:

View File

@ -151,12 +151,6 @@ const TRANSLATIONS = {
heading: "向我解释", heading: "向我解释",
body: "AnythingLLM 的好处", body: "AnythingLLM 的好处",
}, },
pfp: {
title: "助理头像",
description: "为此工作区自定义助手的个人资料图像。",
image: "工作区图像",
remove: "移除工作区图像",
},
delete: { delete: {
title: "删除工作区", title: "删除工作区",
description: "删除此工作区及其所有数据。这将删除所有用户的工作区。", description: "删除此工作区及其所有数据。这将删除所有用户的工作区。",
@ -368,10 +362,6 @@ const TRANSLATIONS = {
title: "名称", title: "名称",
description: "设置所有用户在登录页面看到的名称。", description: "设置所有用户在登录页面看到的名称。",
}, },
"chat-message-alignment": {
title: "聊天消息对齐方式",
description: "选择在聊天界面中使用的消息对齐模式。",
},
"display-language": { "display-language": {
title: "显示语言", title: "显示语言",
description: "选择显示 AnythingLLM 界面所用的语言(若有翻译可用)。", description: "选择显示 AnythingLLM 界面所用的语言(若有翻译可用)。",
@ -713,8 +703,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "发送消息", send_message: "发送消息",
attach_file: "向此对话附加文件", attach_file: "向此对话附加文件",
slash: "查看所有可用的聊天斜杠命令。",
agents: "查看所有可用的聊天助手。",
text_size: "更改文字大小。", text_size: "更改文字大小。",
microphone: "语音输入你的提示。", microphone: "语音输入你的提示。",
send: "将提示消息发送到工作区", send: "将提示消息发送到工作区",
@ -725,18 +713,11 @@ const TRANSLATIONS = {
regenerate_response: "重新回应", regenerate_response: "重新回应",
good_response: "反应良好", good_response: "反应良好",
more_actions: "更多操作", more_actions: "更多操作",
hide_citations: "隐藏引文",
show_citations: "显示引文",
fork: "分叉", fork: "分叉",
delete: "删除", delete: "删除",
save_submit: "提交保存",
cancel: "取消", cancel: "取消",
edit_prompt: "编辑问题", edit_prompt: "编辑问题",
edit_response: "编辑回应", edit_response: "编辑回应",
at_agent: "@agent",
default_agent_description: " - 此工作区的预设代理。",
custom_agents_coming_soon: "自定义代理功能即将推出!",
slash_reset: "/reset",
preset_reset_description: "清除聊天纪录并开始新的聊天", preset_reset_description: "清除聊天纪录并开始新的聊天",
add_new_preset: "新增预设", add_new_preset: "新增预设",
command: "指令", command: "指令",
@ -758,6 +739,34 @@ const TRANSLATIONS = {
missing_credentials: "缺少凭证", missing_credentials: "缺少凭证",
missing_credentials_description: "缺少凭证说明", 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: { profile_settings: {
edit_account: "编辑帐户", edit_account: "编辑帐户",

View File

@ -143,12 +143,6 @@ const TRANSLATIONS = {
heading: "請向我說明", heading: "請向我說明",
body: "AnythingLLM 的優點", body: "AnythingLLM 的優點",
}, },
pfp: {
title: "助理個人檔案圖片",
description: "自訂此工作區助理的個人檔案圖片。",
image: "工作區圖片",
remove: "移除工作區圖片",
},
delete: { delete: {
title: "刪除工作區", title: "刪除工作區",
description: "刪除此工作區及其所有資料。這將會為所有使用者刪除該工作區。", description: "刪除此工作區及其所有資料。這將會為所有使用者刪除該工作區。",
@ -613,8 +607,6 @@ const TRANSLATIONS = {
chat_window: { chat_window: {
send_message: "發送訊息", send_message: "發送訊息",
attach_file: "附加檔案到此對話", attach_file: "附加檔案到此對話",
slash: "查看所有可用的斜線指令。",
agents: "查看所有可用的聊天代理。",
text_size: "變更文字大小。", text_size: "變更文字大小。",
microphone: "語音輸入提示。", microphone: "語音輸入提示。",
send: "將提示訊息發送到工作區", send: "將提示訊息發送到工作區",
@ -625,18 +617,11 @@ const TRANSLATIONS = {
regenerate_response: "重新回應", regenerate_response: "重新回應",
good_response: "反應良好", good_response: "反應良好",
more_actions: "更多操作", more_actions: "更多操作",
hide_citations: "隱藏引文",
show_citations: "顯示引文",
fork: "分叉", fork: "分叉",
delete: "刪除", delete: "刪除",
save_submit: "提交保存",
cancel: "取消", cancel: "取消",
edit_prompt: "編輯問題", edit_prompt: "編輯問題",
edit_response: "編輯回應", edit_response: "編輯回應",
at_agent: "@agent",
default_agent_description: " - 此工作區的預設代理。",
custom_agents_coming_soon: "自訂代理功能即將推出!",
slash_reset: "/reset",
preset_reset_description: "清除聊天紀錄並開始新的聊天", preset_reset_description: "清除聊天紀錄並開始新的聊天",
add_new_preset: "新增預設", add_new_preset: "新增預設",
command: "指令", command: "指令",
@ -658,6 +643,34 @@ const TRANSLATIONS = {
missing_credentials: "缺少憑證", missing_credentials: "缺少憑證",
missing_credentials_description: "缺少憑證說明", 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: { profile_settings: {
edit_account: "編輯帳戶", edit_account: "編輯帳戶",
@ -721,10 +734,6 @@ const TRANSLATIONS = {
title: "應用名稱", title: "應用名稱",
description: "設定所有使用者在登入頁面上看到的應用名稱。", description: "設定所有使用者在登入頁面上看到的應用名稱。",
}, },
"chat-message-alignment": {
title: "聊天訊息對齊方式",
description: "選擇使用聊天介面時訊息的對齊模式。",
},
"display-language": { "display-language": {
title: "顯示語言", title: "顯示語言",
description: "選擇 AnythingLLM 使用者介面的顯示語言(如有提供翻譯)。", description: "選擇 AnythingLLM 使用者介面的顯示語言(如有提供翻譯)。",

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LanguagePreference from "../components/LanguagePreference"; import LanguagePreference from "../components/LanguagePreference";
import ThemePreference from "../components/ThemePreference"; import ThemePreference from "../components/ThemePreference";
import { MessageDirection } from "../components/MessageDirection";
export default function InterfaceSettings() { export default function InterfaceSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -28,7 +27,6 @@ export default function InterfaceSettings() {
</div> </div>
<ThemePreference /> <ThemePreference />
<LanguagePreference /> <LanguagePreference />
<MessageDirection />
</div> </div>
</div> </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 QuickActions from "@/components/lib/QuickActions";
import SuggestedMessages from "@/components/lib/SuggestedMessages"; import SuggestedMessages from "@/components/lib/SuggestedMessages";
import useUser from "@/hooks/useUser"; 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"; import { ChatTooltips } from "@/components/WorkspaceChat/ChatContainer/ChatTooltips";
async function getTargetWorkspace() { async function getTargetWorkspace() {
@ -129,7 +131,7 @@ export default function Home() {
return ( return (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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", writeMode = "replace",
}) { }) {
if (autoSubmit) { if (autoSubmit) {
if (writeMode === "append") {
const currentText =
document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
text = currentText + text;
}
if (!text.trim()) return;
submitMessage(text.trim()); submitMessage(text.trim());
return; return;
} }
@ -269,9 +277,11 @@ function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
return ( return (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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 />} {isMobile && <SidebarMobileHeader />}
<TextSizeMenu />
<WorkspaceModelPicker workspaceSlug={workspace?.slug} />
<DnDFileUploaderWrapper> <DnDFileUploaderWrapper>
<div className="flex flex-col h-full w-full items-center justify-center"> <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]"> <div className="flex flex-col items-center w-full max-w-[750px]">
@ -312,7 +322,7 @@ function NoWorkspacesAssigned() {
return ( return (
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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"> <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"> <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 <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
return ( 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 />} {!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
<Home /> <Home />
</div> </div>

View File

@ -31,11 +31,9 @@ function ShowWorkspaceChat() {
if (!_workspace) return setLoading(false); if (!_workspace) return setLoading(false);
const suggestedMessages = await Workspace.getSuggestedMessages(slug); const suggestedMessages = await Workspace.getSuggestedMessages(slug);
const pfpUrl = await Workspace.fetchPfp(slug);
setWorkspace({ setWorkspace({
..._workspace, ..._workspace,
suggestedMessages, suggestedMessages,
pfpUrl,
}); });
setLoading(false); setLoading(false);
localStorage.setItem( localStorage.setItem(
@ -51,7 +49,7 @@ function ShowWorkspaceChat() {
return ( 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 />} {!isMobile && <Sidebar />}
<WorkspaceChatContainer loading={loading} workspace={workspace} /> <WorkspaceChatContainer loading={loading} workspace={workspace} />
</div> </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 WorkspaceName from "./WorkspaceName";
import SuggestedChatMessages from "./SuggestedChatMessages"; import SuggestedChatMessages from "./SuggestedChatMessages";
import DeleteWorkspace from "./DeleteWorkspace"; import DeleteWorkspace from "./DeleteWorkspace";
import WorkspacePfp from "./WorkspacePfp";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
export default function GeneralInfo({ slug }) { export default function GeneralInfo({ slug }) {
@ -65,7 +64,6 @@ export default function GeneralInfo({ slug }) {
/> />
</form> </form>
<SuggestedChatMessages slug={workspace.slug} /> <SuggestedChatMessages slug={workspace.slug} />
<WorkspacePfp workspace={workspace} slug={slug} />
<DeleteWorkspace workspace={workspace} /> <DeleteWorkspace workspace={workspace} />
</div> </div>
); );

View File

@ -76,7 +76,7 @@ function ShowWorkspaceChat() {
const TabContent = TABS[tab]; const TabContent = TABS[tab];
return ( 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 />} {!isMobile && <Sidebar />}
<div <div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} 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() { export function useIsAgentSessionActive() {
const [activeSession, setActiveSession] = useState(false); const [activeSession, setActiveSession] = useState(
() => !!getAgentSessionActive()
);
useEffect(() => { useEffect(() => {
function listenForAgentSession() { function listenForAgentSession() {
if (!window) return; if (!window) return;

View File

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

View File

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

View File

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

View File

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