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:
parent
868358597e
commit
21ac874cfa
@ -3,6 +3,7 @@ import { SidebarSimple } from "@phosphor-icons/react";
|
||||
import paths from "@/utils/paths";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
const SIDEBAR_TOGGLE_STORAGE_KEY = "anythingllm_sidebar_toggle";
|
||||
export const SIDEBAR_TOGGLE_EVENT = "sidebar-toggle";
|
||||
|
||||
/**
|
||||
* Returns the previous state of the sidebar from localStorage.
|
||||
@ -62,6 +63,11 @@ export function useSidebarToggle() {
|
||||
SIDEBAR_TOGGLE_STORAGE_KEY,
|
||||
showSidebar ? "open" : "closed"
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(SIDEBAR_TOGGLE_EVENT, {
|
||||
detail: { open: showSidebar },
|
||||
})
|
||||
);
|
||||
}, [showSidebar]);
|
||||
|
||||
return { showSidebar, setShowSidebar, canToggleSidebar };
|
||||
|
||||
@ -31,7 +31,6 @@ import CustomCell from "./CustomCell.jsx";
|
||||
import Tooltip from "./CustomTooltip.jsx";
|
||||
import { safeJsonParse } from "@/utils/request.js";
|
||||
import renderMarkdown from "@/utils/chat/markdown.js";
|
||||
import { WorkspaceProfileImage } from "../PromptReply/index.jsx";
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { saveAs } from "file-saver";
|
||||
import { useGenerateImage } from "recharts-to-png";
|
||||
@ -41,7 +40,7 @@ const dataFormatter = (number) => {
|
||||
return Intl.NumberFormat("us").format(number).toString();
|
||||
};
|
||||
|
||||
export function Chartable({ props, workspace }) {
|
||||
export function Chartable({ props }) {
|
||||
const [getDivJpeg, { ref }] = useGenerateImage({
|
||||
quality: 1,
|
||||
type: "image/jpeg",
|
||||
@ -387,20 +386,17 @@ export function Chartable({ props, workspace }) {
|
||||
|
||||
if (!!props.chatId) {
|
||||
return (
|
||||
<div className="flex justify-center items-end w-full">
|
||||
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className="flex gap-x-5">
|
||||
<WorkspaceProfileImage workspace={workspace} />
|
||||
<div className="relative w-full">
|
||||
<DownloadGraph onClick={handleDownload} />
|
||||
<div ref={ref}>{renderChart()}</div>
|
||||
<span
|
||||
className={`flex flex-col gap-y-1 mt-2`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(content.caption),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="py-2 px-4 w-full flex flex-col md:max-w-[80%]">
|
||||
<div className="relative w-full">
|
||||
<DownloadGraph onClick={handleDownload} />
|
||||
<div ref={ref}>{renderChart()}</div>
|
||||
<span
|
||||
className="flex flex-col gap-y-1 mt-2"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(content.caption),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -408,20 +404,18 @@ export function Chartable({ props, workspace }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-end w-full">
|
||||
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="py-2 px-4 w-full flex flex-col md:max-w-[80%]">
|
||||
<div className="relative w-full">
|
||||
<DownloadGraph onClick={handleDownload} />
|
||||
<div ref={ref}>{renderChart()}</div>
|
||||
</div>
|
||||
<div className="flex gap-x-5">
|
||||
<span
|
||||
className={`flex flex-col gap-y-1 mt-2`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(content.caption),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="flex flex-col gap-y-1 mt-2"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderMarkdown(content.caption),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Fragment } from "react";
|
||||
import { decode as HTMLDecode } from "he";
|
||||
import truncate from "truncate";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { middleTruncate } from "@/utils/directories";
|
||||
import {
|
||||
CaretRight,
|
||||
FileText,
|
||||
Info,
|
||||
ArrowSquareOut,
|
||||
@ -14,16 +12,41 @@ import {
|
||||
LinkSimple,
|
||||
GitlabLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
import ConfluenceLogo from "@/media/dataConnectors/confluence.png";
|
||||
import DrupalWikiLogo from "@/media/dataConnectors/drupalwiki.png";
|
||||
import ObsidianLogo from "@/media/dataConnectors/obsidian.png";
|
||||
import PaperlessNgxLogo from "@/media/dataConnectors/paperlessngx.png";
|
||||
import { toPercentString } from "@/utils/numbers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import pluralize from "pluralize";
|
||||
import useTextSize from "@/hooks/useTextSize";
|
||||
import { useSourcesSidebar } from "../../SourcesSidebar";
|
||||
|
||||
function combineLikeSources(sources) {
|
||||
const CIRCLE_ICONS = {
|
||||
file: FileText,
|
||||
link: LinkSimple,
|
||||
youtube: YoutubeLogo,
|
||||
github: GithubLogo,
|
||||
gitlab: GitlabLogo,
|
||||
confluence: LinkSimple,
|
||||
drupalwiki: FileText,
|
||||
obsidian: FileText,
|
||||
paperlessNgx: FileText,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a circle with a source type icon inside.
|
||||
* @param {"file"|"link"|"youtube"|"github"|"gitlab"|"confluence"|"drupalwiki"|"obsidian"|"paperlessNgx"} props.type
|
||||
* @param {number} [props.size] - Circle diameter in px
|
||||
* @param {number} [props.iconSize] - Icon size in px
|
||||
*/
|
||||
export function SourceTypeCircle({ type = "file", size = 22, iconSize = 12 }) {
|
||||
const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;
|
||||
return (
|
||||
<div
|
||||
className="bg-white light:bg-slate-100 rounded-full flex items-center justify-center"
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<Icon size={iconSize} weight="bold" className="text-black" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function combineLikeSources(sources) {
|
||||
const combined = {};
|
||||
sources.forEach((source) => {
|
||||
const { id, title, text, chunkSource = "", score = null } = source;
|
||||
@ -42,106 +65,83 @@ function combineLikeSources(sources) {
|
||||
}
|
||||
|
||||
export default function Citations({ sources = [] }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState(null);
|
||||
const {
|
||||
sidebarOpen,
|
||||
openSidebar,
|
||||
closeSidebar,
|
||||
sources: currentSources,
|
||||
} = useSourcesSidebar();
|
||||
const { t } = useTranslation();
|
||||
const { textSizeClass } = useTextSize();
|
||||
|
||||
if (sources.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-4 justify-left">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={`border-none font-semibold text-white/50 light:text-black/50 font-medium italic ${textSizeClass} text-left ml-14 pt-2 ${
|
||||
open ? "pb-2" : ""
|
||||
} hover:text-white/75 hover:light:text-black/75 transition-all duration-300`}
|
||||
>
|
||||
{open
|
||||
? t("chat_window.hide_citations")
|
||||
: t("chat_window.show_citations")}
|
||||
<CaretRight
|
||||
weight="bold"
|
||||
size={14}
|
||||
className={`inline-block ml-1 transform transition-transform duration-300 ${
|
||||
open ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="flex flex-wrap flex-col items-start overflow-x-scroll no-scroll mt-1 ml-14 gap-y-2">
|
||||
{combineLikeSources(sources).map((source, idx) => (
|
||||
<Citation
|
||||
key={source.title || idx.toString()}
|
||||
source={source}
|
||||
onClick={() => setSelectedSource(source)}
|
||||
textSizeClass={textSizeClass}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedSource && (
|
||||
<CitationDetailModal
|
||||
source={selectedSource}
|
||||
onClose={() => setSelectedSource(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const combined = combineLikeSources(sources);
|
||||
const visibleSources = combined.slice(0, 3);
|
||||
const remainingCount = Math.max(0, combined.length - 3);
|
||||
|
||||
const Citation = ({ source, onClick, textSizeClass }) => {
|
||||
const { title, references = 1 } = source;
|
||||
if (!title) return null;
|
||||
const chunkSourceInfo = parseChunkSource(source);
|
||||
const truncatedTitle = chunkSourceInfo?.text ?? middleTruncate(title, 25);
|
||||
const CitationIcon = ICONS.hasOwnProperty(chunkSourceInfo?.icon)
|
||||
? ICONS[chunkSourceInfo.icon]
|
||||
: ICONS.file;
|
||||
function handleOpenSourcesSidebar() {
|
||||
if (sidebarOpen && sources === currentSources) {
|
||||
closeSidebar();
|
||||
} else {
|
||||
openSidebar(sources);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex doc__source gap-x-1 ${textSizeClass}`}
|
||||
onClick={onClick}
|
||||
onClick={handleOpenSourcesSidebar}
|
||||
className="w-fit flex items-center gap-[5px] px-[10px] py-[4px] rounded-full hover:bg-white/5 light:hover:bg-black/5 transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start flex-1 pt-[4px]">
|
||||
<CitationIcon size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-y-[0.2px] px-1">
|
||||
<p
|
||||
className={`!m-0 font-semibold whitespace-nowrap text-theme-text-primary hover:opacity-55 ${textSizeClass}`}
|
||||
>
|
||||
{truncatedTitle}
|
||||
</p>
|
||||
<p
|
||||
className={`!m-0 text-[10px] font-medium text-theme-text-secondary ${textSizeClass}`}
|
||||
>{`${references} ${pluralize("Reference", Number(references) || 1)}`}</p>
|
||||
<span className="text-xs text-white light:text-slate-800">
|
||||
{t("chat_window.sources")}
|
||||
</span>
|
||||
<div
|
||||
className="relative h-[22px]"
|
||||
style={{ width: `${visibleSources.length * 17 + 5}px` }}
|
||||
>
|
||||
{visibleSources.map((source, idx) => {
|
||||
const info = parseChunkSource(source);
|
||||
return (
|
||||
<div
|
||||
key={source.title || idx}
|
||||
className="absolute top-0 size-[22px] rounded-full border-2 border-zinc-800 light:border-white"
|
||||
style={{ left: `${idx * 17}px`, zIndex: 3 - idx }}
|
||||
>
|
||||
<SourceTypeCircle type={info.icon} size={18} iconSize={10} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{remainingCount > 0 && (
|
||||
<span className="text-xs text-white light:text-slate-800">
|
||||
+ {remainingCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function omitChunkHeader(text) {
|
||||
export function omitChunkHeader(text) {
|
||||
if (!text.includes("<document_metadata>")) return text;
|
||||
return text.split("</document_metadata>")[1].trim();
|
||||
}
|
||||
|
||||
function CitationDetailModal({ source, onClose }) {
|
||||
export function CitationDetailModal({ source, onClose }) {
|
||||
const { references, title, chunks } = source;
|
||||
const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={!!source}>
|
||||
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow border-2 border-zinc-700 light:border-slate-300 overflow-hidden">
|
||||
<div className="relative p-6 border-b rounded-t border-zinc-700 light:border-slate-300">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
{isUrl ? (
|
||||
<a
|
||||
href={linkTo}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xl w-[90%] font-semibold text-white whitespace-nowrap hover:underline hover:text-blue-300 flex items-center gap-x-1"
|
||||
className="text-xl w-[90%] font-semibold text-white light:text-slate-900 whitespace-nowrap hover:underline hover:text-blue-300 light:hover:text-blue-600 flex items-center gap-x-1"
|
||||
>
|
||||
<div className="flex items-center gap-x-1 max-w-full overflow-hidden">
|
||||
<h3 className="truncate text-ellipsis whitespace-nowrap overflow-hidden w-full">
|
||||
@ -151,22 +151,26 @@ function CitationDetailModal({ source, onClose }) {
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
<h3 className="text-xl font-semibold text-white light:text-slate-900 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{truncate(title, 45)}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
{references > 1 && (
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
<p className="text-xs text-zinc-400 light:text-slate-500 mt-2">
|
||||
Referenced {references} times.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
|
||||
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-zinc-700 light:hover:bg-slate-200 border-transparent border"
|
||||
>
|
||||
<X size={24} weight="bold" className="text-white" />
|
||||
<X
|
||||
size={24}
|
||||
weight="bold"
|
||||
className="text-white light:text-slate-900"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@ -176,28 +180,31 @@ function CitationDetailModal({ source, onClose }) {
|
||||
<div className="py-7 px-9 space-y-2 flex-col">
|
||||
{chunks.map(({ text, score }, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<div className="pt-6 text-white">
|
||||
<div className="pt-6 text-white light:text-slate-900">
|
||||
<div className="flex flex-col w-full justify-start pb-6 gap-y-1">
|
||||
<p className="text-white whitespace-pre-line">
|
||||
<p className="text-white light:text-slate-900 whitespace-pre-line">
|
||||
{HTMLDecode(omitChunkHeader(text))}
|
||||
</p>
|
||||
|
||||
{!!score && (
|
||||
<div className="w-full flex items-center text-xs text-white/60 gap-x-2 cursor-default">
|
||||
<div className="w-full flex items-center text-xs text-white/60 light:text-slate-500 gap-x-2 cursor-default">
|
||||
<div
|
||||
data-tooltip-id="similarity-score"
|
||||
data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}
|
||||
className="flex items-center gap-x-1"
|
||||
>
|
||||
<Info size={14} />
|
||||
<p>{toPercentString(score)} match</p>
|
||||
<p>
|
||||
{toPercentString(score)}{" "}
|
||||
{t("chat_window.similarity_match")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{idx !== chunks.length - 1 && (
|
||||
<hr className="border-theme-modal-border" />
|
||||
<hr className="border-zinc-700 light:border-slate-300" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
@ -228,7 +235,7 @@ const supportedSources = [
|
||||
* @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options
|
||||
* @returns {{isUrl: boolean, text: string, href: string, icon: string}}
|
||||
*/
|
||||
function parseChunkSource({ title = "", chunks = [] }) {
|
||||
export function parseChunkSource({ title = "", chunks = [] }) {
|
||||
const nullResponse = {
|
||||
isUrl: false,
|
||||
text: null,
|
||||
@ -315,33 +322,3 @@ function parseChunkSource({ title = "", chunks = [] }) {
|
||||
}
|
||||
return nullResponse;
|
||||
}
|
||||
|
||||
const ConfluenceIcon = ({ size = 16, ...props }) => (
|
||||
<img src={ConfluenceLogo} {...props} width={size} height={size} />
|
||||
);
|
||||
const DrupalWikiIcon = ({ size = 16, ...props }) => (
|
||||
<img src={DrupalWikiLogo} {...props} width={size} height={size} />
|
||||
);
|
||||
const ObsidianIcon = ({ size = 16, ...props }) => (
|
||||
<img src={ObsidianLogo} {...props} width={size} height={size} />
|
||||
);
|
||||
const PaperlessNgxIcon = ({ size = 16, ...props }) => (
|
||||
<img
|
||||
src={PaperlessNgxLogo}
|
||||
{...props}
|
||||
width={size}
|
||||
height={size}
|
||||
className="rounded-sm bg-white"
|
||||
/>
|
||||
);
|
||||
const ICONS = {
|
||||
file: FileText,
|
||||
link: LinkSimple,
|
||||
youtube: YoutubeLogo,
|
||||
github: GithubLogo,
|
||||
gitlab: GitlabLogo,
|
||||
confluence: ConfluenceIcon,
|
||||
drupalwiki: DrupalWikiIcon,
|
||||
obsidian: ObsidianIcon,
|
||||
paperlessNgx: PaperlessNgxIcon,
|
||||
};
|
||||
|
||||
@ -40,7 +40,7 @@ function ActionMenu({ chatId, forkThread, isEditing, role }) {
|
||||
<div className="mt-2 -ml-0.5 relative" ref={menuRef}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="border-none text-[var(--theme-sidebar-footer-icon-fill)] hover:text-[var(--theme-sidebar-footer-icon-fill)] transition-colors duration-200"
|
||||
className="border-none text-zinc-300 light:text-slate-500 transition-colors duration-200"
|
||||
data-tooltip-id="action-menu"
|
||||
data-tooltip-content={t("chat_window.more_actions")}
|
||||
aria-label={t("chat_window.more_actions")}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Pencil } from "@phosphor-icons/react";
|
||||
import { Info, Pencil } from "@phosphor-icons/react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Appearance from "@/models/appearance";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -53,14 +53,10 @@ export function EditMessageAction({ chatId = null, role, isEditing }) {
|
||||
? t("chat_window.edit_prompt")
|
||||
: t("chat_window.edit_response")
|
||||
} `}
|
||||
className="border-none text-zinc-300"
|
||||
className="border-none text-zinc-300 light:text-slate-500"
|
||||
aria-label={`Edit ${role === "user" ? t("chat_window.edit_prompt") : t("chat_window.edit_response")}`}
|
||||
>
|
||||
<Pencil
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
size={21}
|
||||
className="mb-1"
|
||||
/>
|
||||
<Pencil size={21} className="mb-1" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@ -75,17 +71,30 @@ export function EditMessageForm({
|
||||
saveChanges,
|
||||
}) {
|
||||
const formRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
function handleSaveMessage(e) {
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.target);
|
||||
const editedMessage = form.get("editedMessage");
|
||||
const editedMessage = formRef.current.value;
|
||||
saveChanges({ editedMessage, chatId, role, attachments });
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
|
||||
);
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const editedMessage = formRef.current.value;
|
||||
saveChanges({
|
||||
editedMessage,
|
||||
chatId,
|
||||
role,
|
||||
attachments,
|
||||
saveOnly: true,
|
||||
});
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
|
||||
);
|
||||
}
|
||||
|
||||
function cancelEdits() {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })
|
||||
@ -94,36 +103,91 @@ export function EditMessageForm({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
if (!formRef?.current) return;
|
||||
formRef.current.focus();
|
||||
adjustTextArea({ target: formRef.current });
|
||||
}, [formRef]);
|
||||
}, []);
|
||||
|
||||
if (role === "user") {
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col w-full max-w-[650px]"
|
||||
>
|
||||
<textarea
|
||||
ref={formRef}
|
||||
name="editedMessage"
|
||||
spellCheck={Appearance.get("enableSpellCheck")}
|
||||
className="text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden"
|
||||
defaultValue={message}
|
||||
onChange={adjustTextArea}
|
||||
/>
|
||||
<EditActionBar
|
||||
onCancel={cancelEdits}
|
||||
onSave={handleSave}
|
||||
isUserMessage
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSaveMessage} className="flex flex-col w-full">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col w-full max-w-[650px]"
|
||||
>
|
||||
<textarea
|
||||
ref={formRef}
|
||||
name="editedMessage"
|
||||
spellCheck={Appearance.get("enableSpellCheck")}
|
||||
className="text-white w-full rounded bg-theme-bg-secondary border border-white/20 active:outline-none focus:outline-none focus:ring-0 pr-16 pl-1.5 pt-1.5 resize-y"
|
||||
className="text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden"
|
||||
defaultValue={message}
|
||||
onChange={adjustTextArea}
|
||||
/>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="border-none px-2 py-1 bg-gray-200 text-gray-700 font-medium rounded-md mr-2 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
{t("chat_window.save_submit")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="border-none px-2 py-1 bg-historical-msg-system text-white font-medium rounded-md hover:bg-historical-msg-user/90 light:hover:text-white focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
|
||||
onClick={cancelEdits}
|
||||
>
|
||||
{t("chat_window.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
<EditActionBar onCancel={cancelEdits} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function EditActionBar({ onCancel, onSave, isUserMessage = false }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mt-2 flex flex-col md:flex-row md:items-center justify-between gap-2 bg-zinc-800 light:bg-slate-200 rounded-lg p-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info
|
||||
size={12}
|
||||
className="shrink-0 mt-0.5 text-zinc-200 light:text-slate-800"
|
||||
/>
|
||||
<span className="text-zinc-200 light:text-slate-800 text-xs leading-4">
|
||||
{isUserMessage
|
||||
? t("chat_window.edit_info_user")
|
||||
: t("chat_window.edit_info_assistant")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-end shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="border-none text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300"
|
||||
>
|
||||
{t("chat_window.cancel")}
|
||||
</button>
|
||||
{isUserMessage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
className="border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300"
|
||||
>
|
||||
{t("chat_window.save")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="border-none bg-zinc-50 light:bg-slate-800 text-zinc-800 light:text-white text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-zinc-200 light:hover:bg-slate-800"
|
||||
>
|
||||
{isUserMessage ? t("chat_window.submit") : t("chat_window.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { formatDateTimeAsMoment } from "@/utils/directories";
|
||||
import { numberWithCommas } from "@/utils/numbers";
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
const MetricsContext = React.createContext();
|
||||
const SHOW_METRICS_KEY = "anythingllm_show_chat_metrics";
|
||||
const SHOW_METRICS_EVENT = "anythingllm_show_metrics_change";
|
||||
@ -116,7 +117,7 @@ export default function RenderMetrics({ metrics = {} }) {
|
||||
// Inherit the showMetricsAutomatically state from the MetricsProvider so the state is shared across all chats
|
||||
const { showMetricsAutomatically, setShowMetricsAutomatically } =
|
||||
useContext(MetricsContext);
|
||||
if (!metrics?.duration || !metrics?.outputTps) return null;
|
||||
if (!metrics?.duration || !metrics?.outputTps || isMobile) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -128,9 +129,9 @@ export default function RenderMetrics({ metrics = {} }) {
|
||||
? "Click to only show metrics when hovering"
|
||||
: "Click to show metrics as soon as they are available"
|
||||
}
|
||||
className={`border-none flex justify-end items-center gap-x-[8px] ${showMetricsAutomatically ? "opacity-100" : "opacity-0"} md:group-hover:opacity-100 transition-all duration-300`}
|
||||
className={`border-none flex md:justify-end items-center gap-x-[8px] -ml-7 ${showMetricsAutomatically ? "opacity-100" : "opacity-0"} md:group-hover:opacity-100 transition-all duration-300`}
|
||||
>
|
||||
<p className="cursor-pointer text-xs font-mono text-theme-text-secondary opacity-50">
|
||||
<p className="cursor-pointer text-xs font-mono text-zinc-400 light:text-slate-500">
|
||||
{buildMetricsString(metrics)}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@ -65,7 +65,7 @@ export default function AsyncTTSMessage({ slug, chatId }) {
|
||||
? t("pause_tts_speech_message")
|
||||
: t("chat_window.tts_speak_message")
|
||||
}
|
||||
className="border-none text-[var(--theme-sidebar-footer-icon-fill)]"
|
||||
className="border-none text-zinc-300 light:text-slate-500"
|
||||
aria-label={speaking ? "Pause speech" : "Speak message"}
|
||||
>
|
||||
{speaking ? (
|
||||
|
||||
@ -3,6 +3,10 @@ import NativeTTSMessage from "./native";
|
||||
import AsyncTTSMessage from "./asyncTts";
|
||||
import PiperTTSMessage from "./piperTTS";
|
||||
|
||||
function WrapTTS({ children }) {
|
||||
return <div className="mx-2">{children}</div>;
|
||||
}
|
||||
|
||||
export default function TTSMessage({ slug, chatId, message }) {
|
||||
const { settings, provider, loading } = useTTSProvider();
|
||||
if (!chatId || loading) return null;
|
||||
@ -11,16 +15,26 @@ export default function TTSMessage({ slug, chatId, message }) {
|
||||
case "openai":
|
||||
case "generic-openai":
|
||||
case "elevenlabs":
|
||||
return <AsyncTTSMessage chatId={chatId} slug={slug} />;
|
||||
return (
|
||||
<WrapTTS>
|
||||
<AsyncTTSMessage chatId={chatId} slug={slug} />
|
||||
</WrapTTS>
|
||||
);
|
||||
case "piper_local":
|
||||
return (
|
||||
<PiperTTSMessage
|
||||
chatId={chatId}
|
||||
voiceId={settings?.TTSPiperTTSVoiceModel}
|
||||
message={message}
|
||||
/>
|
||||
<WrapTTS>
|
||||
<PiperTTSMessage
|
||||
chatId={chatId}
|
||||
voiceId={settings?.TTSPiperTTSVoiceModel}
|
||||
message={message}
|
||||
/>
|
||||
</WrapTTS>
|
||||
);
|
||||
default:
|
||||
return <NativeTTSMessage chatId={chatId} message={message} />;
|
||||
return (
|
||||
<WrapTTS>
|
||||
<NativeTTSMessage chatId={chatId} message={message} />
|
||||
</WrapTTS>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export default function NativeTTSMessage({ chatId, message }) {
|
||||
data-tooltip-content={
|
||||
speaking ? "Pause TTS speech of message" : "TTS Speak message"
|
||||
}
|
||||
className="border-none text-[var(--theme-sidebar-footer-icon-fill)]"
|
||||
className="border-none text-zinc-300 light:text-slate-500"
|
||||
aria-label={speaking ? "Pause speech" : "Speak message"}
|
||||
>
|
||||
{speaking ? (
|
||||
|
||||
@ -18,7 +18,6 @@ const Actions = ({
|
||||
isEditing,
|
||||
role,
|
||||
metrics = {},
|
||||
alignmentCls = "",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
|
||||
@ -30,15 +29,21 @@ const Actions = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex w-full justify-between items-center ${alignmentCls}`}>
|
||||
<div
|
||||
className={`flex w-full flex-wrap items-center gap-y-1 ${role === "user" ? "justify-end" : "justify-between"}`}
|
||||
>
|
||||
<div className="flex justify-start items-center gap-x-[8px]">
|
||||
<CopyMessage message={message} />
|
||||
<div className="md:group-hover:opacity-100 transition-all duration-300 md:opacity-0 flex justify-start items-center gap-x-[8px]">
|
||||
<EditMessageAction
|
||||
chatId={chatId}
|
||||
role={role}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
<div
|
||||
className={`flex justify-start items-center gap-x-[8px] ${role === "user" ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
<CopyMessage message={message} />
|
||||
<EditMessageAction
|
||||
chatId={chatId}
|
||||
role={role}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
</div>
|
||||
{isLastMessage && !isEditing && (
|
||||
<RegenerateMessage
|
||||
regenerateMessage={regenerateMessage}
|
||||
@ -80,11 +85,10 @@ function FeedbackButton({
|
||||
onClick={handleFeedback}
|
||||
data-tooltip-id="feedback-button"
|
||||
data-tooltip-content={tooltipContent}
|
||||
className="text-zinc-300"
|
||||
className="text-zinc-300 light:text-slate-500"
|
||||
aria-label={tooltipContent}
|
||||
>
|
||||
<IconComponent
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
size={20}
|
||||
className="mb-1"
|
||||
weight={isSelected ? "fill" : "regular"}
|
||||
@ -105,21 +109,13 @@ function CopyMessage({ message }) {
|
||||
onClick={() => copyText(message)}
|
||||
data-tooltip-id="copy-assistant-text"
|
||||
data-tooltip-content={t("chat_window.copy")}
|
||||
className="text-zinc-300"
|
||||
className="text-zinc-300 light:text-slate-500"
|
||||
aria-label={t("chat_window.copy")}
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
size={20}
|
||||
className="mb-1"
|
||||
/>
|
||||
<Check size={20} className="mb-1" />
|
||||
) : (
|
||||
<Copy
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
size={20}
|
||||
className="mb-1"
|
||||
/>
|
||||
<Copy size={20} className="mb-1" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@ -136,15 +132,10 @@ function RegenerateMessage({ regenerateMessage, chatId }) {
|
||||
onClick={() => regenerateMessage(chatId)}
|
||||
data-tooltip-id="regenerate-assistant-text"
|
||||
data-tooltip-content={t("chat_window.regenerate_response")}
|
||||
className="border-none text-zinc-300"
|
||||
className="border-none text-zinc-300 light:text-slate-500"
|
||||
aria-label={t("chat_window.regenerate")}
|
||||
>
|
||||
<ArrowsClockwise
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
size={20}
|
||||
className="mb-1"
|
||||
weight="fill"
|
||||
/>
|
||||
<ArrowsClockwise size={20} className="mb-1" weight="fill" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React, { memo } from "react";
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import { Info, Warning } from "@phosphor-icons/react";
|
||||
import UserIcon from "../../../../UserIcon";
|
||||
import Actions from "./Actions";
|
||||
import renderMarkdown from "@/utils/chat/markdown";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
import Citations from "../Citation";
|
||||
import { v4 } from "uuid";
|
||||
import DOMPurify from "@/utils/chat/purify";
|
||||
@ -36,7 +34,6 @@ const HistoricalMessage = ({
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
metrics = {},
|
||||
alignmentCls = "",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isEditing } = useEditMessage({ chatId, role });
|
||||
@ -53,91 +50,120 @@ const HistoricalMessage = ({
|
||||
const isRefusalMessage =
|
||||
role === "assistant" && message === chatQueryRefusalResponse(workspace);
|
||||
|
||||
if (completeDelete) return null;
|
||||
|
||||
if (!!error) {
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
className={`flex justify-center items-end w-full bg-theme-bg-chat`}
|
||||
>
|
||||
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className={`flex gap-x-5 ${alignmentCls}`}>
|
||||
<ProfileImage role={role} workspace={workspace} />
|
||||
<div className="p-2 rounded-lg bg-red-50 text-red-500">
|
||||
<span className="inline-block">
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
|
||||
respond to message.
|
||||
</span>
|
||||
<p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
<div key={uuid} className="flex justify-start w-full">
|
||||
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
|
||||
<div className="p-2 rounded-lg bg-red-50 text-red-500">
|
||||
<span className="inline-block">
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
|
||||
respond to message.
|
||||
</span>
|
||||
<p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (completeDelete) return null;
|
||||
if (role === "user") {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div key={uuid} className="flex justify-end w-full py-4 px-4">
|
||||
<EditMessageForm
|
||||
role={role}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
attachments={attachments}
|
||||
adjustTextArea={adjustTextArea}
|
||||
saveChanges={saveEditedMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
onAnimationEnd={onEndAnimation}
|
||||
className={`${
|
||||
isDeleted ? "animate-remove" : ""
|
||||
} flex justify-center items-end w-full group bg-theme-bg-chat`}
|
||||
>
|
||||
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className={`flex gap-x-5 ${alignmentCls}`}>
|
||||
<div className="flex flex-col items-center">
|
||||
<ProfileImage role={role} workspace={workspace} />
|
||||
<div className="mt-1 -mb-10">
|
||||
{role === "assistant" && (
|
||||
<TTSMessage
|
||||
slug={workspace?.slug}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<EditMessageForm
|
||||
role={role}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
attachments={attachments}
|
||||
adjustTextArea={adjustTextArea}
|
||||
saveChanges={saveEditedMessage}
|
||||
/>
|
||||
) : (
|
||||
<div className="break-words">
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
onAnimationEnd={onEndAnimation}
|
||||
className={`${isDeleted ? "animate-remove" : ""} flex justify-end w-full group`}
|
||||
>
|
||||
<div className="py-4 px-4 flex flex-col items-end">
|
||||
<div className="bg-zinc-800 light:bg-slate-100 rounded-[20px] rounded-br-none px-4 py-3.5 max-w-[600px] [&_p]:m-0">
|
||||
<TruncatableContent>
|
||||
<RenderChatContent
|
||||
role={role}
|
||||
message={message}
|
||||
messageId={uuid}
|
||||
/>
|
||||
{isRefusalMessage && (
|
||||
<Link
|
||||
data-tooltip-id="query-refusal-info"
|
||||
data-tooltip-content={`${t("chat.refusal.tooltip-description")}`}
|
||||
className="!no-underline group !flex w-fit"
|
||||
to={paths.chatModes()}
|
||||
target="_blank"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-x-1 group-hover:opacity-100 opacity-60 w-fit">
|
||||
<Info className="text-theme-text-secondary" />
|
||||
<p className="!m-0 !p-0 text-theme-text-secondary !no-underline text-xs cursor-pointer">
|
||||
{t("chat.refusal.tooltip-title")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<ChatAttachments attachments={attachments} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-x-5 ml-14">
|
||||
</TruncatableContent>
|
||||
</div>
|
||||
<Actions
|
||||
message={message}
|
||||
feedbackScore={feedbackScore}
|
||||
chatId={chatId}
|
||||
slug={workspace?.slug}
|
||||
isLastMessage={isLastMessage}
|
||||
regenerateMessage={regenerateMessage}
|
||||
isEditing={isEditing}
|
||||
role={role}
|
||||
forkThread={forkThread}
|
||||
metrics={metrics}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
onAnimationEnd={onEndAnimation}
|
||||
className={`${isDeleted ? "animate-remove" : ""} flex justify-start w-full group`}
|
||||
>
|
||||
<div className="py-4 px-4 md:pl-0 flex flex-col w-full">
|
||||
{isEditing ? (
|
||||
<EditMessageForm
|
||||
role={role}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
attachments={attachments}
|
||||
adjustTextArea={adjustTextArea}
|
||||
saveChanges={saveEditedMessage}
|
||||
/>
|
||||
) : (
|
||||
<div className="break-words">
|
||||
<RenderChatContent role={role} message={message} messageId={uuid} />
|
||||
{isRefusalMessage && (
|
||||
<Link
|
||||
data-tooltip-id="query-refusal-info"
|
||||
data-tooltip-content={`${t("chat.refusal.tooltip-description")}`}
|
||||
className="!no-underline group !flex w-fit"
|
||||
to={paths.chatModes()}
|
||||
target="_blank"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-x-1 group-hover:opacity-100 opacity-60 w-fit">
|
||||
<Info className="text-theme-text-secondary" />
|
||||
<p className="!m-0 !p-0 text-theme-text-secondary !no-underline text-xs cursor-pointer">
|
||||
{t("chat.refusal.tooltip-title")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<ChatAttachments attachments={attachments} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start md:items-center gap-x-1">
|
||||
<TTSMessage
|
||||
slug={workspace?.slug}
|
||||
chatId={chatId}
|
||||
message={message}
|
||||
/>
|
||||
<Actions
|
||||
message={message}
|
||||
feedbackScore={feedbackScore}
|
||||
@ -149,7 +175,6 @@ const HistoricalMessage = ({
|
||||
role={role}
|
||||
forkThread={forkThread}
|
||||
metrics={metrics}
|
||||
alignmentCls={alignmentCls}
|
||||
/>
|
||||
</div>
|
||||
{role === "assistant" && <Citations sources={sources} />}
|
||||
@ -158,29 +183,6 @@ const HistoricalMessage = ({
|
||||
);
|
||||
};
|
||||
|
||||
function ProfileImage({ role, workspace }) {
|
||||
if (role === "assistant" && workspace.pfpUrl) {
|
||||
return (
|
||||
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
src={workspace.pfpUrl}
|
||||
alt="Workspace profile picture"
|
||||
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UserIcon
|
||||
user={{
|
||||
uid: role === "user" ? userFromStorage()?.username : workspace.slug,
|
||||
}}
|
||||
role={role}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(
|
||||
HistoricalMessage,
|
||||
// Skip re-render the historical message:
|
||||
@ -199,18 +201,73 @@ export default memo(
|
||||
function ChatAttachments({ attachments = [] }) {
|
||||
if (!attachments.length) return null;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-4 mt-4">
|
||||
{attachments.map((item) => (
|
||||
<img
|
||||
alt={`Attachment: ${item.name}`}
|
||||
key={item.name}
|
||||
src={item.contentString}
|
||||
className="max-w-[300px] rounded-md"
|
||||
className="w-[120px] h-[120px] object-cover rounded-lg"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TruncatableContent({ children }) {
|
||||
const contentRef = useRef(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setIsOverflowing(contentRef.current.scrollHeight > 250);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showTruncation = !isExpanded && isOverflowing;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={showTruncation ? "max-h-[250px] overflow-hidden" : ""}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{showTruncation && (
|
||||
<>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-[36px] light:hidden pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(39, 39, 42, 0.00) 0%, rgba(39, 39, 42, 0.65) 50%, #27272A 100%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-[36px] hidden light:block pointer-events-none"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(241, 245, 249, 0.00) 0%, rgba(241, 245, 249, 0.65) 50%, #F1F5F9 100%)",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isOverflowing && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-zinc-300 light:text-slate-700 hover:text-white light:hover:text-slate-900 text-xs font-medium leading-4 mt-2"
|
||||
>
|
||||
{isExpanded ? t("chat_window.see_less") : t("chat_window.see_more")}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const RenderChatContent = memo(
|
||||
({ role, message, messageId }) => {
|
||||
// If the message is not from the assistant, we can render it directly
|
||||
@ -218,7 +275,7 @@ const RenderChatContent = memo(
|
||||
if (role !== "assistant")
|
||||
return (
|
||||
<span
|
||||
className="flex flex-col gap-y-1"
|
||||
className="flex flex-col gap-y-1 text-white light:text-slate-900"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(renderMarkdown(message)),
|
||||
}}
|
||||
@ -252,7 +309,7 @@ const RenderChatContent = memo(
|
||||
<ThoughtChainComponent content={thoughtChain} messageId={messageId} />
|
||||
)}
|
||||
<span
|
||||
className="flex flex-col gap-y-1"
|
||||
className="flex flex-col gap-y-1 text-white light:text-slate-900"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(renderMarkdown(msgToRender)),
|
||||
}}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
/* eslint-disable react-hooks/refs */
|
||||
import { memo, useRef, useEffect } from "react";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import UserIcon from "../../../../UserIcon";
|
||||
import renderMarkdown from "@/utils/chat/markdown";
|
||||
import Citations from "../Citation";
|
||||
import {
|
||||
@ -11,28 +10,14 @@ import {
|
||||
ThoughtChainComponent,
|
||||
} from "../ThoughtContainer";
|
||||
|
||||
const PromptReply = ({
|
||||
uuid,
|
||||
reply,
|
||||
pending,
|
||||
error,
|
||||
workspace,
|
||||
sources = [],
|
||||
}) => {
|
||||
const assistantBackgroundColor = "bg-theme-bg-chat";
|
||||
|
||||
const PromptReply = ({ uuid, reply, pending, error, sources = [] }) => {
|
||||
if (!reply && sources.length === 0 && !pending && !error) return null;
|
||||
|
||||
if (pending) {
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||
>
|
||||
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className="flex gap-x-5">
|
||||
<WorkspaceProfileImage workspace={workspace} />
|
||||
<div className="mt-3 ml-5 dot-falling light:invert"></div>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
|
||||
<div className="mt-3 ml-1 dot-falling light:invert"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -40,61 +25,32 @@ const PromptReply = ({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||
>
|
||||
<div className="py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className="flex gap-x-5">
|
||||
<WorkspaceProfileImage workspace={workspace} />
|
||||
<span
|
||||
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
|
||||
>
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
|
||||
respond to message.
|
||||
<span className="text-xs">Reason: {error || "unknown"}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div className="py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]">
|
||||
<span className="inline-block p-2 rounded-lg bg-red-50 text-red-500">
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not respond
|
||||
to message.
|
||||
<span className="text-xs">Reason: {error || "unknown"}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
className={`flex justify-center items-end w-full ${assistantBackgroundColor}`}
|
||||
>
|
||||
<div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col">
|
||||
<div className="flex gap-x-5">
|
||||
<WorkspaceProfileImage workspace={workspace} />
|
||||
<RenderAssistantChatContent
|
||||
key={`${uuid}-prompt-reply-content`}
|
||||
message={reply}
|
||||
messageId={uuid}
|
||||
/>
|
||||
</div>
|
||||
<div key={uuid} className="flex justify-start w-full">
|
||||
<div className="py-4 pl-0 pr-4 flex flex-col w-full">
|
||||
<RenderAssistantChatContent
|
||||
key={`${uuid}-prompt-reply-content`}
|
||||
message={reply}
|
||||
messageId={uuid}
|
||||
/>
|
||||
<Citations sources={sources} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function WorkspaceProfileImage({ workspace }) {
|
||||
if (!!workspace.pfpUrl) {
|
||||
return (
|
||||
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
|
||||
<img
|
||||
src={workspace.pfpUrl}
|
||||
alt="Workspace profile picture"
|
||||
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <UserIcon user={{ uid: workspace.slug }} role="assistant" />;
|
||||
}
|
||||
|
||||
function RenderAssistantChatContent({ message, messageId }) {
|
||||
const contentRef = useRef("");
|
||||
const thoughtChainRef = useRef(null);
|
||||
|
||||
@ -15,22 +15,25 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="w-full max-w-[80%] flex flex-col">
|
||||
<div className=" w-full max-w-[800px]">
|
||||
<div className="flex justify-center w-full pr-4">
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full">
|
||||
<div
|
||||
onClick={handleExpandClick}
|
||||
style={{ borderRadius: "6px" }}
|
||||
className={`${!previousThoughts?.length ? "" : `${previousThoughts?.length ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
|
||||
style={{
|
||||
transition: "all 0.1s ease-in-out",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
className="relative bg-zinc-800 light:bg-slate-100 p-4"
|
||||
>
|
||||
<div className="w-7 h-7 flex justify-center flex-shrink-0 items-center">
|
||||
<div className="absolute top-4 left-4 w-[18px] h-[18px]">
|
||||
{isThinking ? (
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-8 h-8 scale-150 transition-opacity duration-200 light:invert light:opacity-50"
|
||||
className="w-[18px] h-[18px] scale-[165%] transition-opacity duration-200 light:invert light:opacity-50"
|
||||
data-tooltip-id="agent-thinking"
|
||||
data-tooltip-content="Agent is thinking..."
|
||||
aria-label="Agent is thinking..."
|
||||
@ -41,57 +44,53 @@ export default function StatusResponse({ messages = [], isThinking = false }) {
|
||||
<img
|
||||
src={AgentStatic}
|
||||
alt="Agent complete"
|
||||
className="w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50"
|
||||
className="w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50"
|
||||
data-tooltip-id="agent-thinking"
|
||||
data-tooltip-content="Agent has finished thinking"
|
||||
aria-label="Agent has finished thinking"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${isExpanded ? "" : "max-h-6"}`}
|
||||
{previousThoughts?.length > 0 && (
|
||||
<button
|
||||
onClick={handleExpandClick}
|
||||
className="absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors"
|
||||
data-tooltip-id="expand-cot"
|
||||
data-tooltip-content={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
aria-label={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
>
|
||||
<div className="text-theme-text-secondary font-mono leading-6">
|
||||
{!isExpanded ? (
|
||||
<span className="block w-full truncate mt-[2px]">
|
||||
{currentThought.content}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{previousThoughts.map((thought, index) => (
|
||||
<div
|
||||
key={`cot-${thought.uuid || index}`}
|
||||
className="mb-2"
|
||||
>
|
||||
{thought.content}
|
||||
</div>
|
||||
))}
|
||||
<div>{currentThought.content}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<CaretDown
|
||||
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? "" : "overflow-hidden max-h-[18px]"}`}
|
||||
>
|
||||
<div className="text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px]">
|
||||
{!isExpanded ? (
|
||||
<span className="block w-full truncate">
|
||||
{currentThought.content}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{previousThoughts.map((thought, index) => (
|
||||
<div
|
||||
key={`cot-${thought.uuid || index}`}
|
||||
className="mb-2"
|
||||
>
|
||||
{thought.content}
|
||||
</div>
|
||||
))}
|
||||
<div>{currentThought.content}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{previousThoughts?.length > 0 && (
|
||||
<button
|
||||
onClick={handleExpandClick}
|
||||
data-tooltip-id="expand-cot"
|
||||
data-tooltip-content={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
|
||||
aria-label={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
>
|
||||
<CaretDown
|
||||
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -141,50 +141,63 @@ export const ThoughtChainComponent = forwardRef(
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-end transition-all duration-200 w-full md:max-w-[800px]">
|
||||
<div className="pb-2 w-full flex gap-x-5 flex-col relative">
|
||||
<div
|
||||
style={{
|
||||
transition: "all 0.1s ease-in-out",
|
||||
borderRadius: "6px",
|
||||
}}
|
||||
className={`${isExpanded ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
|
||||
>
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`w-7 h-7 flex justify-center flex-shrink-0 ${!isExpanded ? "items-center" : "items-start pt-[2px]"}`}
|
||||
style={{
|
||||
transition: "all 0.1s ease-in-out",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
className="relative bg-zinc-800 light:bg-slate-100 p-4"
|
||||
>
|
||||
{isThinking || isComplete ? (
|
||||
<>
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className={`w-7 h-7 transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
|
||||
data-tooltip-id="cot-thinking"
|
||||
data-tooltip-content="Model is thinking..."
|
||||
aria-label="Model is thinking..."
|
||||
>
|
||||
<source src={ThinkingAnimation} type="video/webm" />
|
||||
</video>
|
||||
<img
|
||||
src={ThinkingStatic}
|
||||
alt="Thinking complete"
|
||||
className={`w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
|
||||
data-tooltip-id="cot-thinking"
|
||||
data-tooltip-content="Model has finished thinking"
|
||||
aria-label="Model has finished thinking"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={`overflow-hidden transition-all transform duration-300 ease-in-out origin-top ${isExpanded ? "" : "max-h-6"}`}
|
||||
>
|
||||
<div
|
||||
className={`text-theme-text-secondary font-mono leading-6 ${isExpanded ? "-ml-[5.5px] -mt-[4px]" : "mt-[2px]"}`}
|
||||
<div className="absolute top-4 left-4 w-[18px] h-[18px]">
|
||||
{isThinking || isComplete ? (
|
||||
<>
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className={`w-[18px] h-[18px] scale-[115%] transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
|
||||
data-tooltip-id="cot-thinking"
|
||||
data-tooltip-content="Model is thinking..."
|
||||
aria-label="Model is thinking..."
|
||||
>
|
||||
<source src={ThinkingAnimation} type="video/webm" />
|
||||
</video>
|
||||
<img
|
||||
src={ThinkingStatic}
|
||||
alt="Thinking complete"
|
||||
className={`w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
|
||||
data-tooltip-id="cot-thinking"
|
||||
data-tooltip-content="Model has finished thinking"
|
||||
aria-label="Model has finished thinking"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{canExpand && (
|
||||
<button
|
||||
onClick={handleExpandClick}
|
||||
className="absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors"
|
||||
data-tooltip-id="expand-cot"
|
||||
data-tooltip-content={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
aria-label={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
>
|
||||
<CaretDown
|
||||
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? "" : "overflow-hidden max-h-[18px]"}`}
|
||||
>
|
||||
<div className="text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px] [&_p]:m-0">
|
||||
<span
|
||||
className={`block w-full ${!isExpanded ? "truncate" : ""}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
@ -198,25 +211,6 @@ export const ThoughtChainComponent = forwardRef(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{canExpand ? (
|
||||
<button
|
||||
onClick={handleExpandClick}
|
||||
data-tooltip-id="expand-cot"
|
||||
data-tooltip-content={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
|
||||
aria-label={
|
||||
isExpanded ? "Hide thought chain" : "Show thought chain"
|
||||
}
|
||||
>
|
||||
<CaretDown
|
||||
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,7 +20,6 @@ import paths from "@/utils/paths";
|
||||
import Appearance from "@/models/appearance";
|
||||
import useTextSize from "@/hooks/useTextSize";
|
||||
import useChatHistoryScrollHandle from "@/hooks/useChatHistoryScrollHandle";
|
||||
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
|
||||
import { ThoughtExpansionProvider } from "./ThoughtContainer";
|
||||
|
||||
export default forwardRef(function (
|
||||
@ -42,7 +41,6 @@ export default forwardRef(function (
|
||||
const isStreaming = history[history.length - 1]?.animate;
|
||||
const { showScrollbar } = Appearance.getSettings();
|
||||
const { textSizeClass } = useTextSize();
|
||||
const { getMessageAlignment } = useChatMessageAlignment();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserScrolling && (isAtBottom || isStreaming)) {
|
||||
@ -52,7 +50,7 @@ export default forwardRef(function (
|
||||
|
||||
const handleScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const isBottom = scrollHeight - scrollTop === clientHeight;
|
||||
const isBottom = scrollHeight - scrollTop - clientHeight < 2;
|
||||
|
||||
// Detect if this is a user-initiated scroll
|
||||
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
|
||||
@ -98,10 +96,28 @@ export default forwardRef(function (
|
||||
chatId,
|
||||
role,
|
||||
attachments = [],
|
||||
saveOnly = false,
|
||||
}) => {
|
||||
if (!editedMessage) return; // Don't save empty edits.
|
||||
|
||||
// if the edit was a user message, we will auto-regenerate the response and delete all
|
||||
// "Save" on a user message: update the prompt text without regenerating
|
||||
if (role === "user" && saveOnly) {
|
||||
const updatedHistory = [...history];
|
||||
const targetIdx = history.findIndex((msg) => msg.chatId === chatId);
|
||||
if (targetIdx < 0) return;
|
||||
updatedHistory[targetIdx].content = editedMessage;
|
||||
updateHistory(updatedHistory);
|
||||
await Workspace.updateChat(
|
||||
workspace.slug,
|
||||
threadSlug,
|
||||
chatId,
|
||||
editedMessage,
|
||||
"user"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// "Submit" on a user message: auto-regenerate the response and delete all
|
||||
// messages post modified message
|
||||
if (role === "user") {
|
||||
// remove all messages after the edited message
|
||||
@ -133,7 +149,7 @@ export default forwardRef(function (
|
||||
if (targetIdx < 0) return;
|
||||
updatedHistory[targetIdx].content = editedMessage;
|
||||
updateHistory(updatedHistory);
|
||||
await Workspace.updateChatResponse(
|
||||
await Workspace.updateChat(
|
||||
workspace.slug,
|
||||
threadSlug,
|
||||
chatId,
|
||||
@ -163,7 +179,6 @@ export default forwardRef(function (
|
||||
regenerateAssistantMessage,
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
getMessageAlignment,
|
||||
}),
|
||||
[
|
||||
workspace,
|
||||
@ -191,36 +206,38 @@ export default forwardRef(function (
|
||||
return (
|
||||
<ThoughtExpansionProvider>
|
||||
<div
|
||||
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col justify-start ${showScrollbar ? "show-scrollbar" : "no-scroll"}`}
|
||||
className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col items-center justify-start ${showScrollbar ? "show-scrollbar" : "no-scroll"}`}
|
||||
id="chat-history"
|
||||
ref={chatHistoryRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{compiledHistory.map((item, index) =>
|
||||
Array.isArray(item) ? renderStatusResponse(item, index) : item
|
||||
)}
|
||||
<div className="w-full max-w-[750px]">
|
||||
{compiledHistory.map((item, index) =>
|
||||
Array.isArray(item) ? renderStatusResponse(item, index) : item
|
||||
)}
|
||||
</div>
|
||||
{showing && (
|
||||
<ManageWorkspace
|
||||
hideModal={hideModal}
|
||||
providedSlug={workspace.slug}
|
||||
/>
|
||||
)}
|
||||
{!isAtBottom && (
|
||||
<div className="fixed bottom-40 right-10 md:right-20 z-50 cursor-pointer animate-pulse">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"
|
||||
onClick={() => {
|
||||
scrollToBottom(isStreaming ? false : true);
|
||||
setIsUserScrolling(false);
|
||||
}}
|
||||
>
|
||||
<ArrowDown weight="bold" className="text-white/60 w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
{!isAtBottom && (
|
||||
<div className="absolute bottom-40 right-10 z-50 cursor-pointer animate-pulse">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"
|
||||
onClick={() => {
|
||||
scrollToBottom(isStreaming ? false : true);
|
||||
setIsUserScrolling(false);
|
||||
}}
|
||||
>
|
||||
<ArrowDown weight="bold" className="text-white/60 w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ThoughtExpansionProvider>
|
||||
);
|
||||
});
|
||||
@ -245,7 +262,6 @@ const getLastMessageInfo = (history) => {
|
||||
* @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.
|
||||
* @param {Function} param0.saveEditedMessage - The function to save the edited message.
|
||||
* @param {Function} param0.forkThread - The function to fork the thread.
|
||||
* @param {Function} param0.getMessageAlignment - The function to get the alignment of the message (returns class).
|
||||
* @returns {Array} The compiled history of messages.
|
||||
*/
|
||||
function buildMessages({
|
||||
@ -254,7 +270,6 @@ function buildMessages({
|
||||
regenerateAssistantMessage,
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
getMessageAlignment,
|
||||
}) {
|
||||
return history.reduce((acc, props, index) => {
|
||||
const isLastBotReply =
|
||||
@ -270,9 +285,7 @@ function buildMessages({
|
||||
}
|
||||
|
||||
if (props.type === "rechartVisualize" && !!props.content) {
|
||||
acc.push(
|
||||
<Chartable key={props.uuid} workspace={workspace} props={props} />
|
||||
);
|
||||
acc.push(<Chartable key={props.uuid} props={props} />);
|
||||
} else if (isLastBotReply && props.animate) {
|
||||
acc.push(
|
||||
<PromptReply
|
||||
@ -282,7 +295,6 @@ function buildMessages({
|
||||
pending={props.pending}
|
||||
sources={props.sources}
|
||||
error={props.error}
|
||||
workspace={workspace}
|
||||
closed={props.closed}
|
||||
/>
|
||||
);
|
||||
@ -304,7 +316,6 @@ function buildMessages({
|
||||
saveEditedMessage={saveEditedMessage}
|
||||
forkThread={forkThread}
|
||||
metrics={props.metrics}
|
||||
alignmentCls={getMessageAlignment?.(props.role)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Set the tooltips for the chat container in bulk.
|
||||
@ -16,6 +17,8 @@ import { createPortal } from "react-dom";
|
||||
* @returns
|
||||
*/
|
||||
export function ChatTooltips() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@ -96,6 +99,13 @@ export function ChatTooltips() {
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs"
|
||||
/>
|
||||
<Tooltip
|
||||
id="agent-skill-disabled-tooltip"
|
||||
place="top"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
content={t("chat_window.agent_skills_disabled_in_session")}
|
||||
/>
|
||||
<DocumentLevelTooltip />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { PaperclipHorizontal } from "@phosphor-icons/react";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
@ -98,15 +98,16 @@ export default function AttachItem({
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onPointerEnter={fetchFiles}
|
||||
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer`}
|
||||
className="group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200"
|
||||
>
|
||||
<div className="relative">
|
||||
<PaperclipHorizontal
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className="w-[20px] h-[20px] pointer-events-none text-white rotate-90 -scale-y-100"
|
||||
<Plus
|
||||
size={18}
|
||||
className="pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0"
|
||||
weight="bold"
|
||||
/>
|
||||
{files.length > 0 && (
|
||||
<div className="absolute -top-2 right-[1%] bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center">
|
||||
<div className="absolute -top-2.5 -right-2 bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center">
|
||||
{files.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import useGetProviderModels, {
|
||||
DISABLED_PROVIDERS,
|
||||
} from "@/hooks/useGetProvidersModels";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChatModelSelection({
|
||||
provider,
|
||||
@ -11,110 +10,81 @@ export default function ChatModelSelection({
|
||||
}) {
|
||||
const { defaultModels, customModels, loading } =
|
||||
useGetProviderModels(provider);
|
||||
const { t } = useTranslation();
|
||||
if (DISABLED_PROVIDERS.includes(provider)) return null;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
{t("chat_window.workspace_llm_manager.available_models", {
|
||||
provider,
|
||||
})}
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{t(
|
||||
"chat_window.workspace_llm_manager.available_models_description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
required={true}
|
||||
disabled={true}
|
||||
className="border-theme-modal-border border border-solid bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
>
|
||||
<option disabled={true} selected={true}>
|
||||
-- waiting for models --
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<select
|
||||
required={true}
|
||||
disabled={true}
|
||||
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-not-allowed"
|
||||
>
|
||||
<option disabled={true} selected={true}>
|
||||
-- waiting for models --
|
||||
</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
{t("chat_window.workspace_llm_manager.available_models", {
|
||||
provider,
|
||||
<select
|
||||
id="workspace-llm-model-select"
|
||||
required={true}
|
||||
value={selectedLLMModel}
|
||||
onChange={(e) => {
|
||||
setHasChanges(true);
|
||||
setSelectedLLMModel(e.target.value);
|
||||
}}
|
||||
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-pointer"
|
||||
>
|
||||
{defaultModels.length > 0 && (
|
||||
<optgroup label="General models">
|
||||
{defaultModels.map((model) => {
|
||||
return (
|
||||
<option
|
||||
key={model}
|
||||
value={model}
|
||||
selected={selectedLLMModel === model}
|
||||
>
|
||||
{model}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{t("chat_window.workspace_llm_manager.available_models_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
id="workspace-llm-model-select"
|
||||
required={true}
|
||||
value={selectedLLMModel}
|
||||
onChange={(e) => {
|
||||
setHasChanges(true);
|
||||
setSelectedLLMModel(e.target.value);
|
||||
}}
|
||||
className="border-theme-modal-border border border-solid bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
>
|
||||
{defaultModels.length > 0 && (
|
||||
<optgroup label="General models">
|
||||
{defaultModels.map((model) => {
|
||||
return (
|
||||
<option
|
||||
key={model}
|
||||
value={model}
|
||||
selected={selectedLLMModel === model}
|
||||
>
|
||||
{model}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
{Array.isArray(customModels) && customModels.length > 0 && (
|
||||
<optgroup label="Discovered models">
|
||||
{customModels.map((model) => {
|
||||
return (
|
||||
</optgroup>
|
||||
)}
|
||||
{Array.isArray(customModels) && customModels.length > 0 && (
|
||||
<optgroup label="Discovered models">
|
||||
{customModels.map((model) => {
|
||||
return (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={selectedLLMModel === model.id}
|
||||
>
|
||||
{model.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
{/* For providers like TogetherAi where we partition model by creator entity. */}
|
||||
{!Array.isArray(customModels) && Object.keys(customModels).length > 0 && (
|
||||
<>
|
||||
{Object.entries(customModels).map(([organization, models]) => (
|
||||
<optgroup key={organization} label={organization}>
|
||||
{models.map((model) => (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={selectedLLMModel === model.id}
|
||||
>
|
||||
{model.id}
|
||||
{model.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
{/* For providers like TogetherAi where we partition model by creator entity. */}
|
||||
{!Array.isArray(customModels) &&
|
||||
Object.keys(customModels).length > 0 && (
|
||||
<>
|
||||
{Object.entries(customModels).map(([organization, models]) => (
|
||||
<optgroup key={organization} label={organization}>
|
||||
{models.map((model) => (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={selectedLLMModel === model.id}
|
||||
>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</optgroup>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function LLMSelectorSidePanel({
|
||||
@ -9,31 +10,42 @@ export default function LLMSelectorSidePanel({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-[40%] h-full flex flex-col gap-y-1 border-r-2 border-theme-modal-border py-2 px-[5px]">
|
||||
<input
|
||||
id="llm-search-input"
|
||||
type="search"
|
||||
placeholder={t("chat_window.workspace_llm_manager.search")}
|
||||
onChange={onSearchChange}
|
||||
className="search-input bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder outline-none text-sm rounded-lg px-2 py-2 w-full h-[32px] border-theme-modal-border border border-solid"
|
||||
/>
|
||||
<div className="flex flex-col gap-y-2 overflow-y-scroll ">
|
||||
<div className="w-[40%] h-full flex flex-col gap-4 p-2 border-r border-zinc-700 light:border-slate-300">
|
||||
<div className="relative shrink-0 mx-2">
|
||||
<MagnifyingGlass
|
||||
size={14}
|
||||
className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400 light:text-slate-400"
|
||||
weight="bold"
|
||||
/>
|
||||
<input
|
||||
id="llm-search-input"
|
||||
type="search"
|
||||
placeholder={t("chat_window.workspace_llm_manager.search")}
|
||||
onChange={onSearchChange}
|
||||
className="bg-zinc-900 light:bg-white text-white light:text-slate-900 placeholder:text-zinc-500 light:placeholder:text-slate-400 text-sm rounded-lg pl-8 pr-2.5 h-8 w-full outline-none border border-zinc-900 light:border-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 overflow-y-auto min-h-0 flex-1">
|
||||
{availableProviders.map((llm) => (
|
||||
<button
|
||||
key={llm.value}
|
||||
type="button"
|
||||
data-llm-value={llm.value}
|
||||
className={`border-none hover:cursor-pointer hover:bg-theme-checklist-item-bg-hover flex gap-x-2 items-center p-2 rounded-md ${selectedLLMProvider === llm.value ? "bg-theme-checklist-item-bg" : ""}`}
|
||||
className={`border-none cursor-pointer flex gap-2 items-center px-2.5 py-1.5 rounded-md transition-colors ${
|
||||
selectedLLMProvider === llm.value
|
||||
? "bg-zinc-700 light:bg-slate-200"
|
||||
: "hover:bg-zinc-700/50 light:hover:bg-slate-100 bg-transparent"
|
||||
}`}
|
||||
onClick={() => onProviderClick(llm.value)}
|
||||
>
|
||||
<img
|
||||
src={llm.logo}
|
||||
alt={`${llm.name} logo`}
|
||||
className="w-6 h-6 rounded-md"
|
||||
className="w-6 h-6 rounded"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-xs text-theme-text-primary">{llm.name}</div>
|
||||
</div>
|
||||
<span className="text-sm text-white light:text-slate-900">
|
||||
{llm.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { X, WarningCircle } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -93,17 +93,23 @@ export function NoSetupWarning({ showing, onSetupClick }) {
|
||||
if (!showing) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSetupClick}
|
||||
className="border border-blue-500 rounded-lg p-2 flex flex-col items-center gap-y-2 bg-blue-600/10 text-blue-600 hover:bg-blue-600/20 transition-all duration-300"
|
||||
>
|
||||
<p className="text-sm text-center">
|
||||
<b>{t("chat_window.workspace_llm_manager.missing_credentials")}</b>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<WarningCircle
|
||||
size={16}
|
||||
className="text-white light:text-slate-800 shrink-0 mt-0.5"
|
||||
/>
|
||||
<p className="text-[13px] text-white light:text-slate-800 leading-5">
|
||||
{t("chat_window.workspace_llm_manager.missing_credentials")}{" "}
|
||||
<span
|
||||
onClick={onSetupClick}
|
||||
className="text-sky-400 font-semibold cursor-pointer hover:underline"
|
||||
role="button"
|
||||
>
|
||||
{t(
|
||||
"chat_window.workspace_llm_manager.missing_credentials_description"
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs text-center">
|
||||
{t("chat_window.workspace_llm_manager.missing_credentials_description")}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,7 +16,10 @@ import showToast from "@/utils/toast";
|
||||
import Workspace from "@/models/workspace";
|
||||
import System from "@/models/system";
|
||||
|
||||
export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
export default function LLMSelectorModal({
|
||||
workspaceSlug = null,
|
||||
initialProvider = null,
|
||||
}) {
|
||||
const { slug: urlSlug } = useParams();
|
||||
const slug = urlSlug ?? workspaceSlug;
|
||||
const { t } = useTranslation();
|
||||
@ -36,14 +39,22 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
setLoading(true);
|
||||
Promise.all([Workspace.bySlug(slug), System.keys()])
|
||||
.then(([workspace, systemSettings]) => {
|
||||
const selectedLLMProvider =
|
||||
const savedProvider =
|
||||
workspace.chatProvider ?? systemSettings.LLMProvider;
|
||||
const selectedLLMModel = workspace.chatModel ?? systemSettings.LLMModel;
|
||||
const savedModel = workspace.chatModel ?? systemSettings.LLMModel;
|
||||
const providerToSelect = initialProvider ?? savedProvider;
|
||||
|
||||
setSettings(systemSettings);
|
||||
setSelectedLLMProvider(selectedLLMProvider);
|
||||
autoScrollToSelectedLLMProvider(selectedLLMProvider);
|
||||
setSelectedLLMModel(selectedLLMModel);
|
||||
setSelectedLLMProvider(providerToSelect);
|
||||
autoScrollToSelectedLLMProvider(providerToSelect);
|
||||
setSelectedLLMModel(savedModel);
|
||||
|
||||
if (initialProvider && initialProvider !== savedProvider) {
|
||||
setHasChanges(true);
|
||||
setMissingCredentials(
|
||||
hasMissingCredentials(systemSettings, initialProvider)
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [slug]);
|
||||
@ -87,14 +98,18 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
}
|
||||
}
|
||||
|
||||
const providerName =
|
||||
WORKSPACE_LLM_PROVIDERS.find((p) => p.value === selectedLLMProvider)
|
||||
?.name || selectedLLMProvider;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
id="llm-selector-modal"
|
||||
className="w-full h-[500px] p-0 overflow-y-scroll flex flex-col items-center justify-center"
|
||||
className="w-full h-[388px] flex flex-col items-center justify-center gap-2"
|
||||
>
|
||||
<PreLoader size={12} />
|
||||
<p className="text-theme-text-secondary text-sm mt-2">
|
||||
<p className="text-zinc-400 light:text-slate-500 text-sm">
|
||||
{t("chat_window.workspace_llm_manager.loading_workspace_settings")}
|
||||
</p>
|
||||
</div>
|
||||
@ -102,17 +117,36 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id="llm-selector-modal"
|
||||
className="w-full h-[500px] p-0 overflow-y-scroll flex"
|
||||
>
|
||||
<div id="llm-selector-modal" className="w-full h-[388px] flex">
|
||||
<LLMSelectorSidePanel
|
||||
availableProviders={availableProviders}
|
||||
selectedLLMProvider={selectedLLMProvider}
|
||||
onSearchChange={handleSearch}
|
||||
onProviderClick={handleProviderSelection}
|
||||
/>
|
||||
<div className="w-[60%] h-full px-2 flex flex-col gap-y-2">
|
||||
<div className="w-[60%] h-full p-[18px] flex flex-col gap-2.5">
|
||||
<div className="flex flex-col gap-[15px]">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<p className="text-sm font-medium text-white light:text-slate-800">
|
||||
{t("chat_window.workspace_llm_manager.available_models", {
|
||||
provider: providerName,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-zinc-400 light:text-slate-500">
|
||||
{t(
|
||||
"chat_window.workspace_llm_manager.available_models_description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{!missingCredentials && (
|
||||
<ChatModelSelection
|
||||
provider={selectedLLMProvider}
|
||||
setHasChanges={setHasChanges}
|
||||
selectedLLMModel={selectedLLMModel}
|
||||
setSelectedLLMModel={setSelectedLLMModel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<NoSetupWarning
|
||||
showing={missingCredentials}
|
||||
onSetupClick={() => {
|
||||
@ -128,18 +162,12 @@ export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ChatModelSelection
|
||||
provider={selectedLLMProvider}
|
||||
setHasChanges={setHasChanges}
|
||||
selectedLLMModel={selectedLLMModel}
|
||||
setSelectedLLMModel={setSelectedLLMModel}
|
||||
/>
|
||||
{hasChanges && (
|
||||
{hasChanges && !missingCredentials && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={handleSave}
|
||||
className={`border-none text-xs px-4 py-1 font-semibold light:text-[#ffffff] rounded-lg bg-primary-button hover:bg-secondary hover:text-white h-[34px] whitespace-nowrap w-full`}
|
||||
className="border-none text-xs px-4 py-1.5 font-semibold rounded-lg bg-white text-zinc-900 hover:bg-zinc-200 light:bg-slate-800 light:text-white light:hover:bg-slate-700 h-8 w-full cursor-pointer transition-colors mt-auto"
|
||||
>
|
||||
{saving
|
||||
? t("chat_window.workspace_llm_manager.saving")
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -125,15 +125,17 @@ export default function SpeechToText({ sendCommand }) {
|
||||
data-tooltip-content={`${t("chat_window.microphone")} (CTRL + M)`}
|
||||
aria-label={t("chat_window.microphone")}
|
||||
onClick={listening ? endSTTSession : startSTTSession}
|
||||
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer ${
|
||||
!!listening ? "!opacity-100" : ""
|
||||
className={`group border-none relative flex justify-center items-center cursor-pointer w-8 h-8 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200 ${
|
||||
listening ? "bg-zinc-700 light:bg-slate-200" : ""
|
||||
}`}
|
||||
>
|
||||
<Microphone
|
||||
weight="regular"
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className={`w-[20px] h-[20px] pointer-events-none text-theme-text-primary ${
|
||||
listening ? "animate-pulse-glow" : ""
|
||||
size={18}
|
||||
className={`pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0 ${
|
||||
listening
|
||||
? "animate-pulse-glow !text-white light:!text-slate-800"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<Tooltip
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||
import { Stop } from "@phosphor-icons/react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function StopGenerationButton() {
|
||||
const { t } = useTranslation();
|
||||
function emitHaltEvent() {
|
||||
window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));
|
||||
}
|
||||
@ -13,14 +14,11 @@ export default function StopGenerationButton() {
|
||||
type="button"
|
||||
onClick={emitHaltEvent}
|
||||
data-tooltip-id="stop-generation-button"
|
||||
data-tooltip-content="Stop generating response"
|
||||
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white hover:opacity-80 transition-opacity"
|
||||
data-tooltip-content={t("chat_window.stop_generating")}
|
||||
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-8 h-8 bg-white light:bg-slate-800 hover:opacity-80 transition-opacity"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<Stop
|
||||
className="w-[12px] h-[12px] light:text-white text-black"
|
||||
weight="fill"
|
||||
/>
|
||||
<div className="w-3.5 h-3.5 rounded-[4px] bg-zinc-800 light:bg-white" />
|
||||
</button>
|
||||
<Tooltip
|
||||
id="stop-generation-button"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { CMD_REGEX } from ".";
|
||||
import { CMD_REGEX } from "./constants";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AddPresetModal({ isOpen, onClose, onSave }) {
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { CMD_REGEX } from ".";
|
||||
import { CMD_REGEX } from "./constants";
|
||||
|
||||
export default function EditPresetModal({
|
||||
isOpen,
|
||||
@ -0,0 +1 @@
|
||||
export const CMD_REGEX = /[^a-zA-Z0-9_-]/g;
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
@ -1,17 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import SlashCommandsButton, {
|
||||
SlashCommands,
|
||||
useSlashCommands,
|
||||
} from "./SlashCommands";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import debounce from "lodash.debounce";
|
||||
import { ArrowUp } from "@phosphor-icons/react";
|
||||
import { ArrowUp, At } from "@phosphor-icons/react";
|
||||
import StopGenerationButton from "./StopGenerationButton";
|
||||
import AvailableAgentsButton, {
|
||||
AvailableAgents,
|
||||
useAvailableAgents,
|
||||
} from "./AgentMenu";
|
||||
import TextSizeButton from "./TextSizeMenu";
|
||||
import LLMSelectorAction from "./LLMSelector/action";
|
||||
import SpeechToText from "./SpeechToText";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import AttachmentManager from "./Attachments";
|
||||
@ -25,6 +15,9 @@ import useTextSize from "@/hooks/useTextSize";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Appearance from "@/models/appearance";
|
||||
import usePromptInputStorage from "@/hooks/usePromptInputStorage";
|
||||
import ToolsMenu, { TOOLS_MENU_KEYBOARD_EVENT } from "./ToolsMenu";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useIsAgentSessionActive } from "@/utils/chat/agent";
|
||||
|
||||
export const PROMPT_INPUT_ID = "primary-prompt-input";
|
||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||
@ -50,15 +43,18 @@ export default function PromptInput({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { isDisabled } = useIsDisabled();
|
||||
const agentSessionActive = useIsAgentSessionActive();
|
||||
const [promptInput, setPromptInput] = useState("");
|
||||
const { showAgents, setShowAgents } = useAvailableAgents();
|
||||
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
|
||||
const [showTools, setShowTools] = useState(false);
|
||||
const autoOpenedToolsRef = useRef(false);
|
||||
const toolsHighlightRef = useRef(-1);
|
||||
const formRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
const [_, setFocused] = useState(false);
|
||||
const undoStack = useRef([]);
|
||||
const redoStack = useRef([]);
|
||||
const { textSizeClass } = useTextSize();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Synchronizes prompt input value with localStorage, scoped to the current thread.
|
||||
usePromptInputStorage({
|
||||
@ -66,6 +62,18 @@ export default function PromptInput({
|
||||
setPromptInput,
|
||||
});
|
||||
|
||||
/*
|
||||
* @checklist-item
|
||||
* If the URL has the agent param, open the agent menu for the user
|
||||
* automatically when the component mounts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (searchParams.get("action") === "set-agent-chat") {
|
||||
sendCommand({ text: "@agent " });
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
}, [textareaRef.current]);
|
||||
|
||||
/**
|
||||
* To prevent too many re-renders we remotely listen for updates from the parent
|
||||
* via an event cycle. Otherwise, using message as a prop leads to a re-render every
|
||||
@ -75,6 +83,8 @@ export default function PromptInput({
|
||||
function handlePromptUpdate(e) {
|
||||
const { messageContent, writeMode = "replace" } = e?.detail ?? {};
|
||||
if (writeMode === "append") setPromptInput((prev) => prev + messageContent);
|
||||
else if (writeMode === "prepend")
|
||||
setPromptInput((prev) => messageContent + " " + prev);
|
||||
else setPromptInput(messageContent ?? "");
|
||||
}
|
||||
|
||||
@ -106,7 +116,10 @@ export default function PromptInput({
|
||||
const debouncedSaveState = debounce(saveCurrentState, 250);
|
||||
|
||||
function handleSubmit(e) {
|
||||
// Ignore submits from portaled modals (slash command preset forms)
|
||||
if (e.target !== e.currentTarget) return;
|
||||
setFocused(false);
|
||||
setShowTools(false);
|
||||
submit(e);
|
||||
}
|
||||
|
||||
@ -115,31 +128,63 @@ export default function PromptInput({
|
||||
textareaRef.current.style.height = "auto";
|
||||
}
|
||||
|
||||
function checkForSlash(e) {
|
||||
const input = e.target.value;
|
||||
if (input === "/") setShowSlashCommand(true);
|
||||
if (showSlashCommand) setShowSlashCommand(false);
|
||||
return;
|
||||
}
|
||||
const watchForSlash = debounce(checkForSlash, 300);
|
||||
|
||||
function checkForAt(e) {
|
||||
const input = e.target.value;
|
||||
if (input === "@") return setShowAgents(true);
|
||||
if (showAgents) return setShowAgents(false);
|
||||
}
|
||||
const watchForAt = debounce(checkForAt, 300);
|
||||
|
||||
/**
|
||||
* Capture enter key press to handle submission, redo, or undo
|
||||
* via keyboard shortcuts
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
function captureEnterOrUndo(event) {
|
||||
// Forward keyboard events to the ToolsMenu when open
|
||||
if (showTools) {
|
||||
if (
|
||||
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {
|
||||
detail: { key: event.key },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
// When an item is highlighted via arrow keys, Enter selects it.
|
||||
// Otherwise, Enter falls through to submit the form normally.
|
||||
if (event.key === "Enter" && toolsHighlightRef.current >= 0) {
|
||||
event.preventDefault();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {
|
||||
detail: { key: "Enter" },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
setShowTools(false);
|
||||
textareaRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// "/" toggles the Tools menu only when the input is empty
|
||||
if (
|
||||
event.key === "/" &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
promptInput.trim() === ""
|
||||
) {
|
||||
setShowTools((prev) => {
|
||||
autoOpenedToolsRef.current = !prev;
|
||||
return !prev;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Is simple enter key press w/o shift key
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled
|
||||
setShowTools(false);
|
||||
return submit(event);
|
||||
}
|
||||
|
||||
@ -252,10 +297,15 @@ export default function PromptInput({
|
||||
|
||||
function handleChange(e) {
|
||||
debouncedSaveState(-1);
|
||||
watchForSlash(e);
|
||||
watchForAt(e);
|
||||
adjustTextArea(e);
|
||||
setPromptInput(e.target.value);
|
||||
const value = e.target.value;
|
||||
setPromptInput(value);
|
||||
|
||||
// Auto-dismiss the tools menu when the "/" that opened it is modified
|
||||
if (autoOpenedToolsRef.current && showTools && value !== "/") {
|
||||
setShowTools(false);
|
||||
autoOpenedToolsRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -263,23 +313,9 @@ export default function PromptInput({
|
||||
className={
|
||||
centered
|
||||
? "w-full relative flex justify-center items-center"
|
||||
: "w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center pwa:pb-5"
|
||||
: "w-full fixed md:absolute bottom-0 left-0 z-10 flex justify-center items-center pwa:pb-5"
|
||||
}
|
||||
>
|
||||
<SlashCommands
|
||||
showing={showSlashCommand}
|
||||
setShowing={setShowSlashCommand}
|
||||
sendCommand={sendCommand}
|
||||
promptRef={textareaRef}
|
||||
centered={centered}
|
||||
/>
|
||||
<AvailableAgents
|
||||
showing={showAgents}
|
||||
setShowing={setShowAgents}
|
||||
sendCommand={sendCommand}
|
||||
promptRef={textareaRef}
|
||||
centered={centered}
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className={
|
||||
@ -291,80 +327,72 @@ export default function PromptInput({
|
||||
<div
|
||||
className={`flex items-center rounded-lg md:w-full ${centered ? "mb-0" : "mb-4"}`}
|
||||
>
|
||||
<div className="w-[95vw] md:w-[750px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-[20px] pwa:rounded-3xl flex flex-col px-2 overflow-hidden">
|
||||
<AttachmentManager attachments={attachments} />
|
||||
<div className="flex items-center mx-[7px]">
|
||||
<textarea
|
||||
id={PROMPT_INPUT_ID}
|
||||
ref={textareaRef}
|
||||
onChange={handleChange}
|
||||
onKeyDown={captureEnterOrUndo}
|
||||
onPaste={(e) => {
|
||||
saveCurrentState();
|
||||
handlePasteEvent(e);
|
||||
}}
|
||||
required={true}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
adjustTextArea(e);
|
||||
}}
|
||||
value={promptInput}
|
||||
spellCheck={Appearance.get("enableSpellCheck")}
|
||||
className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 pt-[12px] w-full leading-5 text-white bg-transparent placeholder:text-white/60 light:placeholder:text-theme-text-primary resize-none active:outline-none focus:outline-none flex-grow mb-1 pwa:!text-[16px] ${textSizeClass}`}
|
||||
placeholder={t("chat_window.send_message")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pt-3.5 pb-3 mx-[7px]">
|
||||
<div className="flex gap-x-2 items-center h-5 -ml-[4.5px]">
|
||||
<AttachItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceThreadSlug={threadSlug}
|
||||
<div className="relative w-[95vw] md:w-[750px]">
|
||||
<ToolsMenu
|
||||
showing={showTools}
|
||||
setShowing={setShowTools}
|
||||
sendCommand={sendCommand}
|
||||
promptRef={textareaRef}
|
||||
centered={centered}
|
||||
highlightedIndexRef={toolsHighlightRef}
|
||||
/>
|
||||
<div className="bg-zinc-800 light:bg-white light:border light:border-slate-300 rounded-[20px] pwa:rounded-3xl flex flex-col px-5 overflow-hidden">
|
||||
<AttachmentManager attachments={attachments} />
|
||||
<div className="flex items-center">
|
||||
<textarea
|
||||
id={PROMPT_INPUT_ID}
|
||||
ref={textareaRef}
|
||||
onChange={handleChange}
|
||||
onKeyDown={captureEnterOrUndo}
|
||||
onPaste={(e) => {
|
||||
saveCurrentState();
|
||||
handlePasteEvent(e);
|
||||
}}
|
||||
required={true}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
adjustTextArea(e);
|
||||
}}
|
||||
value={promptInput}
|
||||
spellCheck={Appearance.get("enableSpellCheck")}
|
||||
className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] pt-[20px] w-full leading-5 text-white light:text-slate-600 bg-transparent placeholder:text-white/60 light:placeholder:text-slate-400 resize-none active:outline-none focus:outline-none flex-grow pwa:!text-[16px] ${textSizeClass}`}
|
||||
placeholder={t("chat_window.send_message")}
|
||||
/>
|
||||
<SlashCommandsButton
|
||||
showing={showSlashCommand}
|
||||
setShowSlashCommand={setShowSlashCommand}
|
||||
/>
|
||||
<AvailableAgentsButton
|
||||
showing={showAgents}
|
||||
setShowAgents={setShowAgents}
|
||||
/>
|
||||
<TextSizeButton />
|
||||
<LLMSelectorAction workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center h-5">
|
||||
<SpeechToText sendCommand={sendCommand} />
|
||||
{isStreaming ? (
|
||||
<StopGenerationButton />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white disabled:cursor-not-allowed disabled:opacity-50 hover:opacity-80 transition-opacity"
|
||||
data-tooltip-id="send-prompt"
|
||||
data-tooltip-content={
|
||||
isDisabled
|
||||
? t("chat_window.attachments_processing")
|
||||
: t("chat_window.send")
|
||||
}
|
||||
aria-label={t("chat_window.send")}
|
||||
>
|
||||
<ArrowUp
|
||||
className="w-[12px] h-[12px] pointer-events-none light:text-white text-black"
|
||||
weight="bold"
|
||||
/>
|
||||
<span className="sr-only">Send message</span>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="send-prompt"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
<div className="flex justify-between items-center pt-3.5 pb-3">
|
||||
<div className="flex items-center gap-x-0.25">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<AttachItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceThreadSlug={threadSlug}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AgentSessionButton
|
||||
sendCommand={sendCommand}
|
||||
promptInput={promptInput}
|
||||
textareaRef={textareaRef}
|
||||
visible={!agentSessionActive}
|
||||
/>
|
||||
</div>
|
||||
<ToolsButton
|
||||
showTools={showTools}
|
||||
setShowTools={setShowTools}
|
||||
textareaRef={textareaRef}
|
||||
autoOpenedToolsRef={autoOpenedToolsRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<SpeechToText sendCommand={sendCommand} />
|
||||
{isStreaming ? (
|
||||
<StopGenerationButton />
|
||||
) : (
|
||||
<SendPromptButton
|
||||
formRef={formRef}
|
||||
promptInput={promptInput}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -374,6 +402,123 @@ export default function PromptInput({
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSessionButton({
|
||||
sendCommand,
|
||||
promptInput,
|
||||
textareaRef,
|
||||
visible = true,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!visible) return null;
|
||||
|
||||
function handleClick() {
|
||||
try {
|
||||
if (promptInput?.trim()?.startsWith("@agent")) return;
|
||||
sendCommand({ text: "@agent", writeMode: "prepend" });
|
||||
} finally {
|
||||
textareaRef?.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
data-tooltip-id="agent-session"
|
||||
data-tooltip-content={t("chat_window.start_agent_session")}
|
||||
aria-label={t("chat_window.start_agent_session")}
|
||||
className="group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200"
|
||||
>
|
||||
<At
|
||||
size={18}
|
||||
className="pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0"
|
||||
/>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="agent-session"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolsButton({
|
||||
showTools,
|
||||
setShowTools,
|
||||
textareaRef,
|
||||
autoOpenedToolsRef,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
id="tools-btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
autoOpenedToolsRef.current = false;
|
||||
setShowTools(!showTools);
|
||||
textareaRef.current?.focus();
|
||||
}}
|
||||
className={`group border-none cursor-pointer flex items-center justify-center h-6 px-2 rounded-full ${
|
||||
showTools
|
||||
? "bg-zinc-700 light:bg-slate-200"
|
||||
: "hover:bg-zinc-700 light:hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
showTools
|
||||
? "text-white light:text-slate-800"
|
||||
: "text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-800"
|
||||
}`}
|
||||
>
|
||||
{t("chat_window.tools")}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SendPromptButton({ formRef, promptInput, isDisabled }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
disabled={isDisabled || !promptInput.trim().length}
|
||||
className={`border-none flex justify-center items-center rounded-full w-8 h-8 transition-all ${
|
||||
promptInput.trim().length && !isDisabled
|
||||
? "cursor-pointer bg-white hover:bg-zinc-200 light:bg-slate-800 light:hover:bg-slate-600"
|
||||
: "cursor-not-allowed bg-zinc-600 light:bg-slate-400"
|
||||
}`}
|
||||
data-tooltip-id="send-prompt"
|
||||
data-tooltip-content={
|
||||
isDisabled
|
||||
? t("chat_window.attachments_processing")
|
||||
: t("chat_window.send")
|
||||
}
|
||||
aria-label={t("chat_window.send")}
|
||||
>
|
||||
<ArrowUp
|
||||
className="w-[18px] h-[18px] pointer-events-none text-zinc-800 light:text-white"
|
||||
weight="bold"
|
||||
/>
|
||||
<span className="sr-only">{t("chat_window.send")}</span>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="send-prompt"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event listeners to prevent the send button from being used
|
||||
* for whatever reason that may we may want to prevent the user from sending a message.
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -15,6 +15,7 @@ import handleSocketResponse, {
|
||||
websocketURI,
|
||||
AGENT_SESSION_END,
|
||||
AGENT_SESSION_START,
|
||||
setAgentSessionActive,
|
||||
} from "@/utils/chat/agent";
|
||||
import DnDFileUploaderWrapper from "./DnDWrapper";
|
||||
import SpeechRecognition, {
|
||||
@ -24,11 +25,15 @@ import { ChatTooltips } from "./ChatTooltips";
|
||||
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
|
||||
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
|
||||
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
||||
import { clearPromptInputDraft } from "@/hooks/usePromptInputStorage";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import paths from "@/utils/paths";
|
||||
import QuickActions from "@/components/lib/QuickActions";
|
||||
import SuggestedMessages from "@/components/lib/SuggestedMessages";
|
||||
import TextSizeMenu from "./TextSizeMenu";
|
||||
import WorkspaceModelPicker from "./WorkspaceModelPicker";
|
||||
import SourcesSidebar, { SourcesSidebarProvider } from "./SourcesSidebar";
|
||||
|
||||
export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
const navigate = useNavigate();
|
||||
@ -66,6 +71,10 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
document.getElementById(PROMPT_INPUT_ID)?.value || "";
|
||||
if (!currentMessage) return false;
|
||||
|
||||
// Clear the localStorage draft for this thread/workspace so that if the
|
||||
// PromptInput remounts (empty→chat transition), it won't restore stale text
|
||||
clearPromptInputDraft(threadSlug ?? workspace.slug);
|
||||
|
||||
const prevChatHistory = [
|
||||
...chatHistory,
|
||||
{
|
||||
@ -118,7 +127,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
* @param {boolean} options.autoSubmit - Determines if the text should be sent immediately or if it should be added to the message state (default: false)
|
||||
* @param {Object[]} options.history - The history of the chat prior to this message for overriding the current chat history
|
||||
* @param {Object[import("./DnDWrapper").Attachment]} options.attachments - The attachments to send to the LLM for this message
|
||||
* @param {'replace' | 'append'} options.writeMode - Replace current text or append to existing text (default: replace)
|
||||
* @param {'replace' | 'append' | 'prepend'} options.writeMode - Replace current text or append to existing text (default: replace)
|
||||
* @returns {void}
|
||||
*/
|
||||
const sendCommand = async ({
|
||||
@ -134,6 +143,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (writeMode === "prepend") {
|
||||
const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
|
||||
text = currentText + " " + text;
|
||||
}
|
||||
|
||||
// If we are auto-submitting in append mode
|
||||
// than we need to update text with whatever is in the prompt input + the text we are sending.
|
||||
// @note: `message` will not work here since it is not updated yet.
|
||||
@ -144,6 +158,12 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
}
|
||||
|
||||
if (!text || text === "") return false;
|
||||
|
||||
// Clear the localStorage draft so that if the PromptInput remounts
|
||||
// (e.g. /reset causing empty→chat or chat→empty transitions),
|
||||
// it won't restore stale text.
|
||||
clearPromptInputDraft(threadSlug ?? workspace.slug);
|
||||
|
||||
// If we are auto-submitting
|
||||
// Then we can replace the current text since this is not accumulating.
|
||||
let prevChatHistory;
|
||||
@ -250,17 +270,20 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
|
||||
// TODO: Simplify this WSS stuff
|
||||
useEffect(() => {
|
||||
let socket = null;
|
||||
|
||||
function handleWSS() {
|
||||
try {
|
||||
if (!socketId || !!websocket) return;
|
||||
const socket = new WebSocket(
|
||||
socket = new WebSocket(
|
||||
`${websocketURI()}/api/agent-invocation/${socketId}`
|
||||
);
|
||||
socket.supportsAgentStreaming = false;
|
||||
|
||||
window.addEventListener(ABORT_STREAM_EVENT, () => {
|
||||
setAgentSessionActive(false);
|
||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
||||
websocket.close();
|
||||
socket?.close();
|
||||
});
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
@ -269,6 +292,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
handleSocketResponse(socket, event, setChatHistory);
|
||||
} catch {
|
||||
console.error("Failed to parse data");
|
||||
setAgentSessionActive(false);
|
||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
||||
socket.close();
|
||||
}
|
||||
@ -276,6 +300,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
});
|
||||
|
||||
socket.addEventListener("close", (_event) => {
|
||||
setAgentSessionActive(false);
|
||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
||||
setChatHistory((prev) => [
|
||||
...prev.filter((msg) => !!msg.content),
|
||||
@ -296,6 +321,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
setSocketId(null);
|
||||
});
|
||||
setWebsocket(socket);
|
||||
setAgentSessionActive(true);
|
||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
|
||||
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
||||
} catch (e) {
|
||||
@ -319,6 +345,14 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
}
|
||||
}
|
||||
handleWSS();
|
||||
|
||||
return () => {
|
||||
if (socket) {
|
||||
setAgentSessionActive(false);
|
||||
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
}, [socketId]);
|
||||
|
||||
const isEmpty =
|
||||
@ -328,9 +362,11 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<TextSizeMenu />
|
||||
<WorkspaceModelPicker workspaceSlug={workspace.slug} />
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center w-full max-w-[750px]">
|
||||
@ -369,35 +405,42 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll no-scroll z-[2]"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="contents">
|
||||
<MetricsProvider>
|
||||
<ChatHistory
|
||||
ref={chatHistoryRef}
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
/>
|
||||
</MetricsProvider>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loadingResponse}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
centered={false}
|
||||
/>
|
||||
</div>
|
||||
<SourcesSidebarProvider>
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="relative flex md:ml-[2px] md:mr-[16px] md:my-[16px] w-full h-full z-[2]"
|
||||
>
|
||||
<TextSizeMenu />
|
||||
<div className="flex-1 min-w-0 transition-all duration-500 relative md:rounded-[16px] bg-zinc-900 light:bg-white text-white light:text-slate-900 h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border">
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<WorkspaceModelPicker workspaceSlug={workspace.slug} />
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full pb-20 md:pb-0">
|
||||
<div className="contents">
|
||||
<MetricsProvider>
|
||||
<ChatHistory
|
||||
ref={chatHistoryRef}
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
/>
|
||||
</MetricsProvider>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loadingResponse}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
centered={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DnDFileUploaderWrapper>
|
||||
<ChatTooltips />
|
||||
</div>
|
||||
</DnDFileUploaderWrapper>
|
||||
<ChatTooltips />
|
||||
</div>
|
||||
<SourcesSidebar />
|
||||
</div>
|
||||
</SourcesSidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -24,6 +24,20 @@ import { safeJsonParse } from "@/utils/request";
|
||||
* @param {Function} props.setPromptInput - State setter function for prompt input
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* Immediately clears the stored draft for a given thread/workspace key.
|
||||
* Used before state updates that may remount PromptInput to prevent
|
||||
* stale text from being restored.
|
||||
* @param {string} storageKey - thread slug or workspace slug
|
||||
*/
|
||||
export function clearPromptInputDraft(storageKey) {
|
||||
try {
|
||||
const map = safeJsonParse(localStorage.getItem(USER_PROMPT_INPUT_MAP), {});
|
||||
map[storageKey] = "";
|
||||
localStorage.setItem(USER_PROMPT_INPUT_MAP, JSON.stringify(map));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export default function usePromptInputStorage({ promptInput, setPromptInput }) {
|
||||
const { threadSlug = null, slug: workspaceSlug } = useParams();
|
||||
useEffect(() => {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -326,25 +326,6 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.doc__source {
|
||||
opacity: 0;
|
||||
animation-delay: 50ms;
|
||||
animation: citationAnimation 0.15s ease-out 0s forwards;
|
||||
}
|
||||
|
||||
@keyframes citationAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.sidebar-items:after {
|
||||
@ -677,7 +658,7 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply !bg-black !text-white !py-2 !px-3 !rounded-md;
|
||||
@apply !bg-black !text-white !py-2 !px-3 !rounded-md !z-10;
|
||||
}
|
||||
|
||||
.Toastify__toast-body {
|
||||
|
||||
@ -148,12 +148,6 @@ const TRANSLATIONS = {
|
||||
heading: "اشرح لي",
|
||||
body: "فوائد برنامج إيني ثينك إلْلْمْ",
|
||||
},
|
||||
pfp: {
|
||||
title: "صورة الملف الشخصي للمساعد",
|
||||
description: "تخصيص صورة الملف الشخصي للمساعد لمساحة العمل هذه.",
|
||||
image: "صورة مساحة العمل",
|
||||
remove: "إزالة صورة مساحة العمل",
|
||||
},
|
||||
delete: {
|
||||
title: "حذف مساحة العمل",
|
||||
description:
|
||||
@ -648,8 +642,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "أرسل رسالة",
|
||||
attach_file: "أرفق ملفًا بهذا الدردشة",
|
||||
slash: "عرض جميع الأوامر المتاحة للتواصل.",
|
||||
agents: "عرض جميع الوكلاء المتاحين الذين يمكنك استخدامهم للمحادثة.",
|
||||
text_size: "تغيير حجم النص.",
|
||||
microphone: "اذكر طلبك.",
|
||||
send: "أرسل رسالة فورية إلى مساحة العمل",
|
||||
@ -660,18 +652,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "أعد الرد",
|
||||
good_response: "رد جيد",
|
||||
more_actions: "إجراءات إضافية",
|
||||
hide_citations: "إخفاء المراجع",
|
||||
show_citations: "عرض المراجع",
|
||||
fork: "شوكة",
|
||||
delete: "حذف",
|
||||
save_submit: "حفظ وإرسال",
|
||||
cancel: "إلغاء",
|
||||
edit_prompt: "اقتراح التحرير",
|
||||
edit_response: "عدّل الرد",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "- الوكيل الافتراضي لهذا المساحة.",
|
||||
custom_agents_coming_soon: "سيصل وكلاء مخصصون قريباً!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "امسح سجل الدردشة الخاص بك وابدأ محادثة جديدة",
|
||||
add_new_preset: "إضافة إعداد مسبق",
|
||||
command: "أمر",
|
||||
@ -693,6 +678,35 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "هذا المزود لا يمتلك المؤهلات اللازمة!",
|
||||
missing_credentials_description: "انقر لإعداد بيانات الاعتماد",
|
||||
},
|
||||
submit: "إرسال",
|
||||
edit_info_user:
|
||||
'"إرسال" يعيد إنشاء استجابة الذكاء الاصطناعي. "حفظ" يقوم بتحديث رسالتك فقط.',
|
||||
edit_info_assistant: "سيتم حفظ التغييرات مباشرة في هذا الرد.",
|
||||
see_less: "اقرأ المزيد",
|
||||
see_more: "عرض المزيد",
|
||||
tools: "الأدوات",
|
||||
browse: "تصفح",
|
||||
text_size_label: "حجم النص",
|
||||
select_model: "اختر الطراز",
|
||||
sources: "مصادر",
|
||||
document: "وثيقة",
|
||||
similarity_match: "مباراة",
|
||||
source_count_one: "{{count}}، المرجع",
|
||||
source_count_other: "{{count}} المرجع",
|
||||
preset_exit_description: "إيقاف الجلسة الحالية للمتصفح",
|
||||
add_new: "أضف جديدًا",
|
||||
edit: "تحرير",
|
||||
publish: "نشر",
|
||||
stop_generating: "توقف عن إنشاء رد",
|
||||
pause_tts_speech_message: "توقف عن قراءة النص بصوت مسجل.",
|
||||
slash_commands: "أوامر مختصرة",
|
||||
agent_skills: "مهارات الوكيل",
|
||||
manage_agent_skills: "إدارة مهارات الوكلاء",
|
||||
agent_skills_disabled_in_session:
|
||||
'لا يمكن تعديل المهارات أثناء جلسة مع عامل. يجب عليك أولاً استخدام الأمر "/exit" لإنهاء الجلسة.',
|
||||
start_agent_session: "ابدأ جلسة الممثل",
|
||||
use_agent_session_to_use_tools:
|
||||
"يمكنك استخدام الأدوات المتاحة في الدردشة عن طريق بدء جلسة مع ممثل خدمة العملاء باستخدام الرمز '@agent' في بداية رسالتك.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "تحرير الحساب",
|
||||
@ -758,10 +772,6 @@ const TRANSLATIONS = {
|
||||
title: "اسم",
|
||||
description: "حدد اسمًا يظهر في صفحة تسجيل الدخول لجميع المستخدمين.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "مواءمة رسائل الدردشة",
|
||||
description: "حدد وضع محاذاة الرسائل عند استخدام واجهة الدردشة.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "اللغة المعروضة",
|
||||
description:
|
||||
|
||||
@ -164,13 +164,6 @@ const TRANSLATIONS = {
|
||||
heading: "Vysvětlit mi",
|
||||
body: "výhody AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Profilový obrázek asistenta",
|
||||
description:
|
||||
"Přizpůsobte profilový obrázek asistenta pro tento pracovní prostor.",
|
||||
image: "Obrázek pracovního prostoru",
|
||||
remove: "Odebrat obrázek pracovního prostoru",
|
||||
},
|
||||
delete: {
|
||||
title: "Smazat pracovní prostor",
|
||||
description:
|
||||
@ -398,11 +391,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Nastavte název, který je zobrazen na přihlašovací stránce všem uživatelům.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Zarovnání zpráv chatu",
|
||||
description:
|
||||
"Vyberte režim zarovnání zpráv při použití rozhraní chatu.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Zobrazovací jazyk",
|
||||
description:
|
||||
@ -794,9 +782,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Přílohy se zpracovávají. Prosím čekejte...",
|
||||
send_message: "Odeslat zprávu",
|
||||
attach_file: "Přiložit soubor k tomuto chatu",
|
||||
slash: "Zobrazit všechny dostupné lomítkové příkazy pro chatování.",
|
||||
agents:
|
||||
"Zobrazit všechny dostupné agenty, které můžete použít pro chatování.",
|
||||
text_size: "Změnit velikost textu.",
|
||||
microphone: "Mluvit svou výzvu.",
|
||||
send: "Odeslat zprávu výzvy do pracovního prostoru",
|
||||
@ -806,18 +791,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Regenerovat odpověď",
|
||||
good_response: "Dobrá odpověď",
|
||||
more_actions: "Další akce",
|
||||
hide_citations: "Skrýt citace",
|
||||
show_citations: "Zobrazit citace",
|
||||
fork: "Rozdělit",
|
||||
delete: "Smazat",
|
||||
save_submit: "Uložit a odeslat",
|
||||
cancel: "Zrušit",
|
||||
edit_prompt: "Upravit výzvu",
|
||||
edit_response: "Upravit odpověď",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - výchozí agent pro tento pracovní prostor.",
|
||||
custom_agents_coming_soon: "vlastní agenti přicházejí brzy!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Vymazat historii chatu a začít nový chat",
|
||||
add_new_preset: " Přidat novou předvolbu",
|
||||
command: "Příkaz",
|
||||
@ -841,6 +819,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Klikněte pro nastavení přihlašovacích údajů",
|
||||
},
|
||||
submit: "Odeslat",
|
||||
edit_info_user:
|
||||
"„Odeslat“ znovu vygeneruje odpověď od AI. „Uložit“ aktualizuje pouze vaši zprávu.",
|
||||
edit_info_assistant: "Vaše změny budou uloženy přímo v tomto odpovědi.",
|
||||
see_less: "Zobrazit méně",
|
||||
see_more: "Více",
|
||||
tools: "Nářadí",
|
||||
browse: "Prohlédněte si",
|
||||
text_size_label: "Velikost písma",
|
||||
select_model: "Vyberte model",
|
||||
sources: "Zdroje",
|
||||
document: "Dokument",
|
||||
similarity_match: "zápas",
|
||||
source_count_one: "{{count}} – odkaz",
|
||||
source_count_other: "{{count}} – odkazy",
|
||||
preset_exit_description: "Zastavte aktuální relaci s agentem",
|
||||
add_new: "Přidat nové",
|
||||
edit: "Upravit",
|
||||
publish: "Publikovat",
|
||||
stop_generating: "Zastavte generování odpovědi",
|
||||
pause_tts_speech_message:
|
||||
"Zastavte čtení textu pomocí syntetické řeči z tohoto zprávy.",
|
||||
slash_commands: "Příkazy v řádku",
|
||||
agent_skills: "Dovednosti agenta",
|
||||
manage_agent_skills: "Řízení dovedností agentů",
|
||||
agent_skills_disabled_in_session:
|
||||
"Není možné upravovat dovednosti během aktivního sezení s agentem. Nejprve použijte příkaz `/exit` pro ukončení sezení.",
|
||||
start_agent_session: "Spustit relaci s agentem",
|
||||
use_agent_session_to_use_tools:
|
||||
"Můžete využít nástroje v chatu spuštěním sezení s agentem pomocí příkazu '@agent' na začátku vašeho vstupu.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Upravit účet",
|
||||
|
||||
@ -150,12 +150,6 @@ const TRANSLATIONS = {
|
||||
heading: "Forklar mig",
|
||||
body: "fordelene ved AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Assistentens profilbillede",
|
||||
description: "Tilpas assistentens profilbillede for dette arbejdsområde.",
|
||||
image: "Arbejdsområdebillede",
|
||||
remove: "Fjern arbejdsområdebillede",
|
||||
},
|
||||
delete: {
|
||||
title: "Slet arbejdsområde",
|
||||
description:
|
||||
@ -656,8 +650,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Send en besked",
|
||||
attach_file: "Vedhæft en fil til denne chat",
|
||||
slash: "Vis alle tilgængelige skråstreg-kommandoer til chat.",
|
||||
agents: "Vis alle tilgængelige agenter, du kan bruge til chat.",
|
||||
text_size: "Ændr tekststørrelse.",
|
||||
microphone: "Tal din prompt.",
|
||||
send: "Send promptbesked til arbejdsområdet",
|
||||
@ -669,18 +661,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Genopbyg svar",
|
||||
good_response: "Godt svar",
|
||||
more_actions: "Flere handlinger",
|
||||
hide_citations: "Skjul henvisninger",
|
||||
show_citations: "Vis henvisninger",
|
||||
fork: "Fork",
|
||||
delete: "Slet",
|
||||
save_submit: "Gem og indsende",
|
||||
cancel: "Annullér",
|
||||
edit_prompt: "Redigeringsanmodning",
|
||||
edit_response: "Rediger svar",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "- standardagenten for dette arbejdsområde.",
|
||||
custom_agents_coming_soon: "Specialagenter kommer snart!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Rydd op i din chat-historik og start en ny samtale",
|
||||
add_new_preset: "Tilføj ny forudindstilling",
|
||||
@ -706,6 +691,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Klik for at oprette legitimationsoplysninger",
|
||||
},
|
||||
submit: "Indsend",
|
||||
edit_info_user:
|
||||
'"Send" genopretter AI-responsen. "Gem" opdaterer kun dit budskab.',
|
||||
edit_info_assistant:
|
||||
"Ændringerne, du laver, vil blive gemt direkte i dette svar.",
|
||||
see_less: "Se mindre",
|
||||
see_more: "Se flere",
|
||||
tools: "Værktøj",
|
||||
browse: "Gennemse",
|
||||
text_size_label: "Tekststørrelse",
|
||||
select_model: "Vælg model",
|
||||
sources: "Kilder",
|
||||
document: "Dokument",
|
||||
similarity_match: "kamp",
|
||||
source_count_one: "{{count}} henvisning",
|
||||
source_count_other: "{{count}} referencer",
|
||||
preset_exit_description: "Afslut den aktuelle agent-session",
|
||||
add_new: "Tilføj nyt",
|
||||
edit: "Rediger",
|
||||
publish: "Udgive",
|
||||
stop_generating: "Stop med at generere svar",
|
||||
pause_tts_speech_message: "Pause TTS-læsningen af beskeden",
|
||||
slash_commands: "Kommandoer",
|
||||
agent_skills: "Agenters kompetencer",
|
||||
manage_agent_skills: "Administrer agenters kompetencer",
|
||||
agent_skills_disabled_in_session:
|
||||
"Det er ikke muligt at ændre færdigheder under en aktiv agent-session. Brug kommandoen `/exit` for at afslutte sessionen først.",
|
||||
start_agent_session: "Start Agent-session",
|
||||
use_agent_session_to_use_tools:
|
||||
"Du kan bruge værktøjer i chat ved at starte en agent-session med '@agent' i starten af din forespørgsel.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Rediger konto",
|
||||
@ -773,10 +788,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Angiv et navn, der vises på login-siden for alle brugere.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Sammenstillet samtale",
|
||||
description: "Vælg alignmentsmoden, når du bruger chat-grænsefladen.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Visningssprog",
|
||||
description:
|
||||
|
||||
@ -157,13 +157,6 @@ const TRANSLATIONS = {
|
||||
heading: "Erkläre mir",
|
||||
body: "die Vorteile von AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Assistent-Profilbild",
|
||||
description:
|
||||
"Passen Sie das Profilbild des Assistenten für diesen Workspace an.",
|
||||
image: "Workspace-Bild",
|
||||
remove: "Workspace-Bild entfernen",
|
||||
},
|
||||
delete: {
|
||||
title: "Workspace löschen",
|
||||
description:
|
||||
@ -392,11 +385,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Geben Sie einen Anwendungsnamen ein, der auf der Login-Seite erscheint.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Nachrichtenanordnung im Chat",
|
||||
description:
|
||||
"Bestimmen Sie den Ausrichtungsmodus der Chat-Nachrichten.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Sprache",
|
||||
description:
|
||||
@ -772,8 +760,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Anhänge werden verarbeitet. Bitte warten...",
|
||||
send_message: "Schreibe eine Nachricht",
|
||||
attach_file: "Füge eine Datei zum Chat hinzu",
|
||||
slash: "Schau dir alle verfügbaren Slash Befehle für den Chat an.",
|
||||
agents: "Schau dir alle verfugbaren Agentenfähigkeiten für den Chat an.",
|
||||
text_size: "Ändere die Größe des Textes.",
|
||||
microphone: "Spreche deinen Prompt ein.",
|
||||
send: "Versende den Prompt an den Workspace.",
|
||||
@ -783,18 +769,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Antwort neu generieren",
|
||||
good_response: "Gute Antwort",
|
||||
more_actions: "Weitere Aktionen",
|
||||
hide_citations: "Quellenangaben ausblenden",
|
||||
show_citations: "Quellenangaben anzeigen",
|
||||
fork: "Abzweigen",
|
||||
delete: "Löschen",
|
||||
save_submit: "Speichern und Senden",
|
||||
cancel: "Abbrechen",
|
||||
edit_prompt: "Prompt bearbeiten",
|
||||
edit_response: "Antwort bearbeiten",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "– Standardagent für diesen Workspace.",
|
||||
custom_agents_coming_soon: "Eigene Agenten bald verfügbar!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Chatverlauf löschen und neuen Chat starten",
|
||||
add_new_preset: "Neues Preset anlegen",
|
||||
command: "Befehl",
|
||||
@ -817,6 +796,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Für diesen Anbieter fehlen Anmeldedaten!",
|
||||
missing_credentials_description: "Klicken, um Zugangsdaten einzurichten",
|
||||
},
|
||||
submit: "Absenden",
|
||||
edit_info_user:
|
||||
'"Absenden" generiert die Antwort des KI-Systems neu. "Speichern" aktualisiert lediglich Ihre Nachricht.',
|
||||
edit_info_assistant:
|
||||
"Ihre Änderungen werden direkt in diese Antwort gespeichert.",
|
||||
see_less: "Weniger anzeigen",
|
||||
see_more: "Mehr anzeigen",
|
||||
tools: "Werkzeuge",
|
||||
browse: "Durchsuchen",
|
||||
text_size_label: "Schriftgröße",
|
||||
select_model: "Modell auswählen",
|
||||
sources: "Quellen",
|
||||
document: "Dokument",
|
||||
similarity_match: "Spiel",
|
||||
source_count_one: "{{count}} Referenz",
|
||||
source_count_other: "{{count}} Verweise",
|
||||
preset_exit_description: "Behalte die aktuelle Agentensitzung",
|
||||
add_new: "Neu hinzufügen",
|
||||
edit: "Bearbeiten",
|
||||
publish: "Veröffentlichen",
|
||||
stop_generating: "Stoppen Sie die Generierung von Antworten",
|
||||
pause_tts_speech_message: "Pause die Text-to-Speech-Funktion der Nachricht",
|
||||
slash_commands: "Befehlszeilen",
|
||||
agent_skills: "Fähigkeiten von Agenten",
|
||||
manage_agent_skills: "Verwalten Sie die Fähigkeiten von Agenten",
|
||||
agent_skills_disabled_in_session:
|
||||
"Es ist nicht möglich, während einer aktiven Sitzung die Fähigkeiten zu ändern. Verwenden Sie zuerst den Befehl `/exit`, um die Sitzung zu beenden.",
|
||||
start_agent_session: "Starte eine Agent-Sitzung",
|
||||
use_agent_session_to_use_tools:
|
||||
'Sie können Tools im Chat nutzen, indem Sie eine Agentensitzung mit "@agent" am Anfang Ihrer Anfrage starten.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Account bearbeiten",
|
||||
|
||||
@ -163,13 +163,6 @@ const TRANSLATIONS = {
|
||||
heading: "Explain to me",
|
||||
body: "the benefits of AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Assistant Profile Image",
|
||||
description:
|
||||
"Customize the profile image of the assistant for this workspace.",
|
||||
image: "Workspace Image",
|
||||
remove: "Remove Workspace Image",
|
||||
},
|
||||
delete: {
|
||||
title: "Delete Workspace",
|
||||
description:
|
||||
@ -397,11 +390,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Set a name that is displayed on the login page to all users.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Chat Message Alignment",
|
||||
description:
|
||||
"Select the message alignment mode when using the chat interface.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Display Language",
|
||||
description:
|
||||
@ -795,8 +783,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Attachments are processing. Please wait...",
|
||||
send_message: "Send a message",
|
||||
attach_file: "Attach a file to this chat",
|
||||
slash: "View all available slash commands for chatting.",
|
||||
agents: "View all available agents you can use for chatting.",
|
||||
text_size: "Change text size.",
|
||||
microphone: "Speak your prompt.",
|
||||
send: "Send prompt message to workspace",
|
||||
@ -806,20 +792,31 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Regenerate response",
|
||||
good_response: "Good response",
|
||||
more_actions: "More actions",
|
||||
hide_citations: "Hide citations",
|
||||
show_citations: "Show citations",
|
||||
sources: "Sources",
|
||||
source_count_one: "{{count}} reference",
|
||||
source_count_other: "{{count}} references",
|
||||
document: "Document",
|
||||
similarity_match: "match",
|
||||
pause_tts_speech_message: "Pause TTS speech of message",
|
||||
fork: "Fork",
|
||||
delete: "Delete",
|
||||
save_submit: "Save & Submit",
|
||||
cancel: "Cancel",
|
||||
submit: "Submit",
|
||||
edit_prompt: "Edit prompt",
|
||||
edit_response: "Edit response",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - the default agent for this workspace.",
|
||||
custom_agents_coming_soon: "custom agents are coming soon!",
|
||||
slash_reset: "/reset",
|
||||
edit_info_user:
|
||||
'"Submit" regenerates the AI response. "Save" updates your message only.',
|
||||
edit_info_assistant:
|
||||
"Your changes will be saved directly to this response.",
|
||||
see_less: "See Less",
|
||||
see_more: "See More",
|
||||
preset_reset_description: "Clear your chat history and begin a new chat",
|
||||
preset_exit_description: "Halt the current agent session",
|
||||
add_new_preset: " Add New Preset",
|
||||
add_new: "Add new",
|
||||
edit: "Edit",
|
||||
publish: "Publish",
|
||||
stop_generating: "Stop generating response",
|
||||
command: "Command",
|
||||
your_command: "your-command",
|
||||
placeholder_prompt:
|
||||
@ -830,15 +827,27 @@ const TRANSLATIONS = {
|
||||
small: "Small",
|
||||
normal: "Normal",
|
||||
large: "Large",
|
||||
tools: "Tools",
|
||||
browse: "Browse",
|
||||
text_size_label: "Text Size",
|
||||
select_model: "Select Model",
|
||||
slash_commands: "Slash Commands",
|
||||
agent_skills: "Agent Skills",
|
||||
manage_agent_skills: "Manage Agent Skills",
|
||||
start_agent_session: "Start Agent Session",
|
||||
agent_skills_disabled_in_session:
|
||||
"Can't modify skills during an active agent session. Use /exit to end the session first.",
|
||||
use_agent_session_to_use_tools:
|
||||
"You can use tools in chat by starting an agent session with '@agent' at the beginning of your prompt.",
|
||||
workspace_llm_manager: {
|
||||
search: "Search LLM providers",
|
||||
search: "Search",
|
||||
loading_workspace_settings: "Loading workspace settings...",
|
||||
available_models: "Available Models for {{provider}}",
|
||||
available_models_description: "Select a model to use for this workspace.",
|
||||
save: "Use this model",
|
||||
saving: "Setting model as workspace default...",
|
||||
missing_credentials: "This provider is missing credentials!",
|
||||
missing_credentials_description: "Click to set up credentials",
|
||||
missing_credentials_description: "Set up now",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
|
||||
@ -158,13 +158,6 @@ const TRANSLATIONS = {
|
||||
heading: "Explícame",
|
||||
body: "los beneficios de AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Imagen de perfil del asistente",
|
||||
description:
|
||||
"Personaliza la imagen de perfil del asistente para este espacio de trabajo.",
|
||||
image: "Imagen del espacio de trabajo",
|
||||
remove: "Eliminar imagen del espacio de trabajo",
|
||||
},
|
||||
delete: {
|
||||
title: "Eliminar espacio de trabajo",
|
||||
description:
|
||||
@ -400,11 +393,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Establece un nombre que se mostrará en la página de inicio de sesión para todos los usuarios.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Alineación de mensajes de chat",
|
||||
description:
|
||||
"Selecciona el modo de alineación de mensajes cuando utilices la interfaz de chat.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Idioma de visualización",
|
||||
description:
|
||||
@ -782,8 +770,6 @@ const TRANSLATIONS = {
|
||||
"Los archivos adjuntos se están procesando. Por favor, espera...",
|
||||
send_message: "Enviar un mensaje",
|
||||
attach_file: "Adjuntar un archivo a este chat",
|
||||
slash: "Ver todos los comandos de barra disponibles para chatear.",
|
||||
agents: "Ver todos los agentes disponibles que puedes usar para chatear.",
|
||||
text_size: "Cambiar tamaño del texto.",
|
||||
microphone: "Habla tu prompt.",
|
||||
send: "Enviar mensaje de prompt al espacio de trabajo",
|
||||
@ -793,19 +779,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Regenerar respuesta",
|
||||
good_response: "Buena respuesta",
|
||||
more_actions: "Más acciones",
|
||||
hide_citations: "Ocultar citas",
|
||||
show_citations: "Mostrar citas",
|
||||
fork: "Bifurcar",
|
||||
delete: "Eliminar",
|
||||
save_submit: "Guardar y enviar",
|
||||
cancel: "Cancelar",
|
||||
edit_prompt: "Editar prompt",
|
||||
edit_response: "Editar respuesta",
|
||||
at_agent: "@agent",
|
||||
default_agent_description:
|
||||
" - el agente predeterminado para este espacio de trabajo.",
|
||||
custom_agents_coming_soon: "¡los agentes personalizados llegarán pronto!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Borra tu historial de chat y comienza un nuevo chat",
|
||||
add_new_preset: " Agregar nuevo preajuste",
|
||||
@ -833,6 +811,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Haz clic para configurar las credenciales",
|
||||
},
|
||||
submit: "Enviar",
|
||||
edit_info_user:
|
||||
'"Enviar" regenera la respuesta de la IA. "Guardar" actualiza solo tu mensaje.',
|
||||
edit_info_assistant:
|
||||
"Los cambios que realice se guardarán directamente en esta respuesta.",
|
||||
see_less: "Ver menos",
|
||||
see_more: "Ver más",
|
||||
tools: "Herramientas",
|
||||
browse: "Explorar",
|
||||
text_size_label: "Tamaño del texto",
|
||||
select_model: "Seleccionar modelo",
|
||||
sources: "Fuentes",
|
||||
document: "Documento",
|
||||
similarity_match: "partido",
|
||||
source_count_one: "{{count}} de referencia",
|
||||
source_count_other: "{{count}} referencias",
|
||||
preset_exit_description: "Detener la sesión actual del agente.",
|
||||
add_new: "Añadir nuevo",
|
||||
edit: "Editar",
|
||||
publish: "Publicar",
|
||||
stop_generating: "Dejar de generar respuestas",
|
||||
pause_tts_speech_message: "Pausa la lectura de voz del mensaje.",
|
||||
slash_commands: "Comandos abreviados",
|
||||
agent_skills: "Habilidades del agente",
|
||||
manage_agent_skills: "Gestionar las habilidades del agente.",
|
||||
agent_skills_disabled_in_session:
|
||||
"No es posible modificar las habilidades durante una sesión con un agente activo. Primero, utilice el comando `/exit` para finalizar la sesión.",
|
||||
start_agent_session: "Iniciar sesión como agente",
|
||||
use_agent_session_to_use_tools:
|
||||
"Puede utilizar las herramientas disponibles en el chat iniciando una sesión con un agente utilizando el prefijo '@agent' al principio de su mensaje.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editar cuenta",
|
||||
|
||||
@ -154,12 +154,6 @@ const TRANSLATIONS = {
|
||||
heading: "Selgita mulle",
|
||||
body: "AnythingLLM eeliseid",
|
||||
},
|
||||
pfp: {
|
||||
title: "Abilise profiilipilt",
|
||||
description: "Kohanda selle tööruumi abilise profiilipilti.",
|
||||
image: "Tööruumi pilt",
|
||||
remove: "Eemalda tööruumi pilt",
|
||||
},
|
||||
delete: {
|
||||
title: "Kustuta tööruum",
|
||||
description:
|
||||
@ -378,10 +372,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Nimi, mis kuvatakse kõigile kasutajatele sisselogimislehel.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Vestlussõnumite joondus",
|
||||
description: "Vali sõnumite joondus vestlusliideses.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Kuvakeel",
|
||||
description:
|
||||
@ -738,8 +728,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Manused töötlevad. Palun oota…",
|
||||
send_message: "Saada sõnum",
|
||||
attach_file: "Lisa fail vestlusele",
|
||||
slash: "Vaata kõiki slash-käske.",
|
||||
agents: "Vaata kõiki agente, keda saad kasutada.",
|
||||
text_size: "Muuda teksti suurust.",
|
||||
microphone: "Esita päring häälega.",
|
||||
send: "Saada päring tööruumi",
|
||||
@ -749,18 +737,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Loo vastus uuesti",
|
||||
good_response: "Hea vastus",
|
||||
more_actions: "Rohkem toiminguid",
|
||||
hide_citations: "Peida viited",
|
||||
show_citations: "Näita viiteid",
|
||||
fork: "Hargnemine",
|
||||
delete: "Kustuta",
|
||||
save_submit: "Salvesta ja saada",
|
||||
cancel: "Tühista",
|
||||
edit_prompt: "Redigeeri päringut",
|
||||
edit_response: "Redigeeri vastust",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " – selle tööruumi vaikimisi agent.",
|
||||
custom_agents_coming_soon: "kohandatud agendid tulekul!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Tühjenda vestlusajalugu ja alusta uut vestlust",
|
||||
add_new_preset: " Lisa uus preset",
|
||||
command: "Käsk",
|
||||
@ -782,6 +763,35 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Sellel pakkujal puuduvad võtmed!",
|
||||
missing_credentials_description: "Klõpsa, et määrata võtmed",
|
||||
},
|
||||
submit: "Saada",
|
||||
edit_info_user:
|
||||
'"Saada" taastab AI vastuse. "Salvesta" muudab ainult teie sõnumi.',
|
||||
edit_info_assistant: "Teie muutused salvestatakse otse sellele vastusele.",
|
||||
see_less: "Näita vähem",
|
||||
see_more: "Vaata rohkem",
|
||||
tools: "Vahendid",
|
||||
browse: "Sirva",
|
||||
text_size_label: "Teksti suurus",
|
||||
select_model: "Valige mudel",
|
||||
sources: "Allikasid",
|
||||
document: "Dokument",
|
||||
similarity_match: "mäng",
|
||||
source_count_one: "{{count}} viidatud",
|
||||
source_count_other: "Viidatud allikad",
|
||||
preset_exit_description: "Lõpeta hetkeseisuga",
|
||||
add_new: "Lisada uus",
|
||||
edit: "Redigeerimine",
|
||||
publish: "Avaldada",
|
||||
stop_generating: "Lõpeta vastuste genereerimine",
|
||||
pause_tts_speech_message: "Peata sõna-sünteesi (TTS) rääkimine sõnumis",
|
||||
slash_commands: "Lihtsasti kasutatavad käsud",
|
||||
agent_skills: "Agentide oskused",
|
||||
manage_agent_skills: "Halda agentide oskusi",
|
||||
agent_skills_disabled_in_session:
|
||||
"Ei ole võimalik muuta oskusi aktiivse agenti seanssi ajal. Enne seanssi lõpetamist kasutage käsku /exit.",
|
||||
start_agent_session: "Alusta agenti sessiooni",
|
||||
use_agent_session_to_use_tools:
|
||||
"Saate kasutada vahendeid vestluses, alustades agenti sessiooni, lisades käskile '@agent' sõna.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Muuda kontot",
|
||||
|
||||
@ -149,12 +149,6 @@ const TRANSLATIONS = {
|
||||
heading: "برایم توضیح بده",
|
||||
body: "مزایای AnythingLLM را",
|
||||
},
|
||||
pfp: {
|
||||
title: "تصویر پروفایل دستیار",
|
||||
description: "تصویر پروفایل دستیار را برای این فضای کاری شخصیسازی کنید.",
|
||||
image: "تصویر فضای کاری",
|
||||
remove: "حذف تصویر فضای کاری",
|
||||
},
|
||||
delete: {
|
||||
title: "حذف فضای کاری",
|
||||
description:
|
||||
@ -654,9 +648,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "یک پیام ارسال کنید",
|
||||
attach_file: "لطفاً یک فایل را به این چت پیوست کنید.",
|
||||
slash: "برای مشاهده تمام دستورات Slash موجود برای چت.",
|
||||
agents:
|
||||
"تمام عوامل موجود را که میتوانید برای گفتگو استفاده کنید، مشاهده کنید.",
|
||||
text_size: "تغییر اندازه متن.",
|
||||
microphone: "سوال خود را بپرسید.",
|
||||
send: "پیام فوری را برای فضای کاری ارسال کنید",
|
||||
@ -667,18 +658,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "بازسازی پاسخ",
|
||||
good_response: "پاسخ خوب",
|
||||
more_actions: "اقدامات بیشتر",
|
||||
hide_citations: "پنهان کردن ارجاعات",
|
||||
show_citations: "نمایش ارجاعات",
|
||||
fork: "چنگال",
|
||||
delete: "حذف",
|
||||
save_submit: "ذخیره و ارسال",
|
||||
cancel: "ยกد",
|
||||
edit_prompt: "لطفاً دستور ویرایش را ارائه دهید.",
|
||||
edit_response: "لطفا پاسخ را ویرایش کنید.",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "- عامل پیشفرض برای این فضای کاری.",
|
||||
custom_agents_coming_soon: "نمایندگان ویژه در حال آمدن هستند!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "حذف تاریخچه چت خود و شروع یک چت جدید",
|
||||
add_new_preset: "اضافه کردن تنظیمات پیشفرض جدید",
|
||||
command: "دستورالعمل",
|
||||
@ -703,6 +687,35 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"برای تنظیم اعتبارها، اینجا را کلیک کنید",
|
||||
},
|
||||
submit: "ارسال",
|
||||
edit_info_user:
|
||||
'"ارسال" پاسخ تولید شده توسط هوش مصنوعی را دوباره ایجاد میکند. "ذخیره" فقط پیام شما را بهروز میکند.',
|
||||
edit_info_assistant: "تغییرات شما مستقیماً در این پاسخ ذخیره خواهند شد.",
|
||||
see_less: "کمی بیشتر",
|
||||
see_more: "بیشتر",
|
||||
tools: "ابزارها",
|
||||
browse: "جستجو",
|
||||
text_size_label: "اندازه متن",
|
||||
select_model: "انتخاب مدل",
|
||||
sources: "منابع",
|
||||
document: "اسناد",
|
||||
similarity_match: "مسابقه",
|
||||
source_count_one: "{{count}}، مرجع",
|
||||
source_count_other: "{{count}}، منابع",
|
||||
preset_exit_description: "متوقف کردن جلسه فعلی با نمایندگی",
|
||||
add_new: "اضافه کردن موارد جدید",
|
||||
edit: "ویرایش",
|
||||
publish: "انتشار",
|
||||
stop_generating: "متوقف کردن تولید پاسخ",
|
||||
pause_tts_speech_message: "مکث در پخش صدای متن",
|
||||
slash_commands: "دستورات کوتاهشده",
|
||||
agent_skills: "مهارتهای کارگزار",
|
||||
manage_agent_skills: "مدیریت مهارتهای نمایندگان",
|
||||
agent_skills_disabled_in_session:
|
||||
"امکان تغییر مهارتها در حین یک جلسه فعال با یک عامل وجود ندارد. ابتدا با استفاده از دستور /exit، جلسه را به پایان برسانید.",
|
||||
start_agent_session: "شروع جلسه با نماینده",
|
||||
use_agent_session_to_use_tools:
|
||||
"شما میتوانید از ابزارهای موجود در چت با شروع یک جلسه با یک عامل از طریق استفاده از '@agent' در ابتدای پیام خود استفاده کنید.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "ویرایش حساب",
|
||||
@ -768,11 +781,6 @@ const TRANSLATIONS = {
|
||||
title: "نام",
|
||||
description: "یک نام را برای تمام کاربران در صفحه ورود مشخص کنید.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "همراهبودن پیامها در چت",
|
||||
description:
|
||||
"هنگام استفاده از رابط چت، حالت همتراز کردن پیام را انتخاب کنید.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "زبان نمایش",
|
||||
description:
|
||||
|
||||
@ -150,13 +150,6 @@ const TRANSLATIONS = {
|
||||
heading: "Expliquez-moi",
|
||||
body: "les avantages de AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Image de profil de l'assistant",
|
||||
description:
|
||||
"Personnalisez l'image de profil de l'assistant pour cet espace de travail.",
|
||||
image: "Image de l'espace de travail",
|
||||
remove: "Supprimer l'image de l'espace de travail",
|
||||
},
|
||||
delete: {
|
||||
title: "Supprimer l'Espace de Travail",
|
||||
description:
|
||||
@ -656,8 +649,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Envoyer un message",
|
||||
attach_file: "Joindre un fichier",
|
||||
slash: "Voir les commandes slash disponibles",
|
||||
agents: "Voir les agents disponibles",
|
||||
text_size: "Modifier la taille du texte",
|
||||
microphone: "Enregistrer un message vocal",
|
||||
send: "Envoyer le message au chatbot",
|
||||
@ -669,18 +660,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Régénérer la réponse",
|
||||
good_response: "Bonne réponse",
|
||||
more_actions: "Plus d'actions",
|
||||
hide_citations: "Masquer les citations",
|
||||
show_citations: "Afficher les citations",
|
||||
fork: "Dupliquer",
|
||||
delete: "Supprimer",
|
||||
save_submit: "Sauvegarder et envoyer",
|
||||
cancel: "Annuler",
|
||||
edit_prompt: "Modifier le prompt",
|
||||
edit_response: "Modifier la réponse",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "l'agent par défaut de cet espace de travail",
|
||||
custom_agents_coming_soon: "Agents personnalisés bientôt disponibles",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Efface l'historique du chat actuel et commence une nouvelle conversation.",
|
||||
add_new_preset: "Ajouter une nouvelle commande preset",
|
||||
@ -706,6 +690,37 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Vous devez configurer vos identifiants de fournisseur LLM avant de pouvoir sélectionner un modèle.",
|
||||
},
|
||||
submit: "Soumettre",
|
||||
edit_info_user:
|
||||
'"Soumettre" permet de régénérer la réponse de l\'IA. "Enregistrer" met uniquement à jour votre message.',
|
||||
edit_info_assistant:
|
||||
"Vos modifications seront enregistrées directement dans cette réponse.",
|
||||
see_less: "Voir moins",
|
||||
see_more: "Voir plus",
|
||||
tools: "Outils",
|
||||
browse: "Parcourir",
|
||||
text_size_label: "Taille du texte",
|
||||
select_model: "Sélectionner le modèle",
|
||||
sources: "Sources",
|
||||
document: "Document",
|
||||
similarity_match: "match",
|
||||
source_count_one: "{{count}} référence",
|
||||
source_count_other: "Références à {{count}}",
|
||||
preset_exit_description: "Arrêter la session actuelle de l'agent",
|
||||
add_new: "Ajouter",
|
||||
edit: "Modifier",
|
||||
publish: "Publier",
|
||||
stop_generating: "Arrêtez de générer des réponses",
|
||||
pause_tts_speech_message:
|
||||
"Mettre en pause la lecture de la voix synthétique du message",
|
||||
slash_commands: "Commandes abrégées",
|
||||
agent_skills: "Compétences des agents",
|
||||
manage_agent_skills: "Gérer les compétences des agents",
|
||||
agent_skills_disabled_in_session:
|
||||
"Il n'est pas possible de modifier les compétences pendant une session avec un agent actif. Utilisez la commande `/exit` pour terminer la session en premier.",
|
||||
start_agent_session: "Démarrer la session de l'agent",
|
||||
use_agent_session_to_use_tools:
|
||||
'Vous pouvez utiliser des outils via le chat en lançant une session avec un agent en utilisant le préfixe "@agent" au début de votre requête.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Modifier le compte",
|
||||
@ -773,10 +788,6 @@ const TRANSLATIONS = {
|
||||
title: "Nom de l'application",
|
||||
description: "Définissez le nom affiché dans l'interface.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Alignement des messages",
|
||||
description: "Choisissez l'alignement des messages dans le chat.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Langue d'affichage",
|
||||
description: "Sélectionnez la langue de l'interface utilisateur.",
|
||||
|
||||
@ -152,12 +152,6 @@ const TRANSLATIONS = {
|
||||
heading: "הסבר לי",
|
||||
body: "את היתרונות של AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "תמונת פרופיל של העוזר",
|
||||
description: "התאם אישית את תמונת הפרופיל של העוזר עבור סביבת עבודה זו.",
|
||||
image: "תמונת סביבת עבודה",
|
||||
remove: "הסר תמונת סביבת עבודה",
|
||||
},
|
||||
delete: {
|
||||
title: "מחק סביבת עבודה",
|
||||
description:
|
||||
@ -379,10 +373,6 @@ const TRANSLATIONS = {
|
||||
title: "שם",
|
||||
description: "הגדר שם שיוצג בדף ההתחברות לכל המשתמשים.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "יישור הודעות צ'אט",
|
||||
description: "בחר את מצב יישור ההודעות בעת שימוש בממשק הצ'אט.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "שפת תצוגה",
|
||||
description:
|
||||
@ -742,8 +732,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "קבצים מצורפים בעיבוד. אנא המתן...",
|
||||
send_message: "שלח הודעה",
|
||||
attach_file: "צרף קובץ לצ'אט זה",
|
||||
slash: "הצג את כל פקודות הסלאש הזמינות לצ'אט.",
|
||||
agents: "הצג את כל הסוכנים הזמינים שתוכל להשתמש בהם לצ'אט.",
|
||||
text_size: "שנה גודל טקסט.",
|
||||
microphone: "אמור את ההנחיה שלך.",
|
||||
send: "שלח הודעת הנחיה לסביבת העבודה",
|
||||
@ -753,18 +741,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "צור תגובה מחדש",
|
||||
good_response: "תגובה טובה",
|
||||
more_actions: "פעולות נוספות",
|
||||
hide_citations: "הסתר ציטוטים",
|
||||
show_citations: "הצג ציטוטים",
|
||||
fork: "פצל (Fork)",
|
||||
delete: "מחק",
|
||||
save_submit: "שמור ושלח",
|
||||
cancel: "בטל",
|
||||
edit_prompt: "ערוך הנחיה",
|
||||
edit_response: "ערוך תגובה",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - סוכן ברירת המחדל עבור סביבת עבודה זו.",
|
||||
custom_agents_coming_soon: "סוכנים מותאמים אישית יגיעו בקרוב!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "נקה את היסטוריית הצ'אט שלך והתחל צ'אט חדש",
|
||||
add_new_preset: " הוסף הגדרה קבועה חדשה",
|
||||
command: "פקודה",
|
||||
@ -786,6 +767,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "חסרים אישורים לספק זה!",
|
||||
missing_credentials_description: "לחץ להגדרת אישורים",
|
||||
},
|
||||
submit: "הגש",
|
||||
edit_info_user:
|
||||
'"שלח" מחזיר את התגובה של הבינה המלאכותית. "שמור" מעדכן רק את ההודעה שלך.',
|
||||
edit_info_assistant: "השינויים שאתם מבצעים יישמרו ישירות בתגובה זו.",
|
||||
see_less: "ראה פחות",
|
||||
see_more: "לראות עוד",
|
||||
tools: "כלים",
|
||||
browse: "גלו",
|
||||
text_size_label: "גודל הטקסט",
|
||||
select_model: "בחר מודל",
|
||||
sources: "מקורות",
|
||||
document: "מסמך",
|
||||
similarity_match: "משחק",
|
||||
source_count_one: "{{count}} - הפניה",
|
||||
source_count_other: "{{count}} – מקורות",
|
||||
preset_exit_description: "עצירת הפעולה הנוכחית של המשתמש",
|
||||
add_new: "הוסף חדש",
|
||||
edit: "עריכה",
|
||||
publish: "להוציא לאור",
|
||||
stop_generating: "הפסיקו ליצור תגובה",
|
||||
pause_tts_speech_message:
|
||||
"השהייה של קריאת טקסט באמצעות תוכנת TTS (Text-to-Speech)",
|
||||
slash_commands: "פקודות קיצור",
|
||||
agent_skills: "כישורים של סוכן",
|
||||
manage_agent_skills: "ניהול מיומנויות של סוכנים",
|
||||
agent_skills_disabled_in_session:
|
||||
'לא ניתן לשנות כישורים במהלך סשן פעיל. יש להשתמש בפקודה "/exit" כדי לסיים את הסשן תחילה.',
|
||||
start_agent_session: "התחלת סשן עם סוכן",
|
||||
use_agent_session_to_use_tools:
|
||||
"ניתן להשתמש בכלי הדיון באמצעות פתיחת סשן עם נציג על ידי שימוש בסימן '@agent' בתחילת ההודעה.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "ערוך חשבון",
|
||||
|
||||
@ -151,13 +151,6 @@ const TRANSLATIONS = {
|
||||
heading: "Spiegami",
|
||||
body: "i vantaggi di AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Immagine del profilo dell'assistente",
|
||||
description:
|
||||
"Personalizza l'immagine del profilo dell'assistente per quest'area di lavoro.",
|
||||
image: "Immagine dell'area di lavoro",
|
||||
remove: "Rimuovi immagine dell'area di lavoro",
|
||||
},
|
||||
delete: {
|
||||
title: "Elimina area di lavoro",
|
||||
description:
|
||||
@ -660,9 +653,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Invia un messaggio",
|
||||
attach_file: "Allega un file a questa chat.",
|
||||
slash: "Visualizza tutti i comandi disponibili per la chat.",
|
||||
agents:
|
||||
"Visualizza tutti gli agenti disponibili che puoi utilizzare per la chat.",
|
||||
text_size: "Modifica la dimensione del testo.",
|
||||
microphone: "Formula la tua richiesta.",
|
||||
send: "Invia un messaggio immediato allo spazio di lavoro",
|
||||
@ -674,19 +664,11 @@ const TRANSLATIONS = {
|
||||
"Per favore, fornisci il testo originale che desideri che venga riformulato.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda è alla ricerca di un nuovo dipendente per ricoprire la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda è alla ricerca di un nuovo dipendente per la posizione di rappresentante commerciale.\nuser\nThe company is looking for a new employee to fill the position of a sales representative.\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\nuser>Regenerate response\nassistant\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.",
|
||||
good_response: "Ottima risposta.",
|
||||
more_actions: "Ulteriori azioni",
|
||||
hide_citations: "Nascondi le citazioni",
|
||||
show_citations: "Mostra citazioni",
|
||||
fork: "Forchetta",
|
||||
delete: "Elimina",
|
||||
save_submit: "Salva e invia",
|
||||
cancel: "Annulla",
|
||||
edit_prompt: "Suggerimento di modifica:",
|
||||
edit_response: "Modifica la risposta",
|
||||
at_agent: "@agent",
|
||||
default_agent_description:
|
||||
"- l'agente predefinito per questo spazio di lavoro.",
|
||||
custom_agents_coming_soon: "Agenti personalizzati in arrivo a breve!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Elimina la cronologia delle chat e avvia una nuova chat",
|
||||
add_new_preset: "Aggiungi nuovo preset",
|
||||
@ -716,6 +698,37 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Fare clic per configurare le credenziali",
|
||||
},
|
||||
submit: "Invia",
|
||||
edit_info_user:
|
||||
'"Invia" rigenera la risposta dell\'IA. "Salva" aggiorna solo il tuo messaggio.',
|
||||
edit_info_assistant:
|
||||
"Le modifiche verranno salvate direttamente in questa risposta.",
|
||||
see_less: "Visualizza meno",
|
||||
see_more: "Visualizza altro",
|
||||
tools: "Strumenti",
|
||||
browse: "Naviga",
|
||||
text_size_label: "Dimensione del testo",
|
||||
select_model: "Seleziona il modello",
|
||||
sources: "Fonti",
|
||||
document: "Documento",
|
||||
similarity_match: "partita",
|
||||
source_count_one: "Riferimento {{count}}",
|
||||
source_count_other: "Riferimenti a {{count}}",
|
||||
preset_exit_description: "Interrompere la sessione corrente con l'agente.",
|
||||
add_new: "Aggiungi nuovo",
|
||||
edit: "Modifica",
|
||||
publish: "Pubblicare",
|
||||
stop_generating: "Interrompi la generazione della risposta",
|
||||
pause_tts_speech_message:
|
||||
"Mettere in pausa la lettura vocale del messaggio",
|
||||
slash_commands: "Comandi abbreviati",
|
||||
agent_skills: "Competenze dell'agente",
|
||||
manage_agent_skills: "Gestire le competenze degli agenti",
|
||||
agent_skills_disabled_in_session:
|
||||
"Non è possibile modificare le competenze durante una sessione di agente attivo. Per terminare la sessione, utilizzare il comando `/exit`.",
|
||||
start_agent_session: "Avvia sessione agente",
|
||||
use_agent_session_to_use_tools:
|
||||
'È possibile utilizzare gli strumenti disponibili tramite chat avviando una sessione con un agente utilizzando il prefisso "@agent" all\'inizio del messaggio.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Modifica account",
|
||||
@ -788,11 +801,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Definisci un nome che verrà visualizzato sulla pagina di accesso per tutti gli utenti.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Allignment di conversazioni",
|
||||
description:
|
||||
"Seleziona la modalità di allineamento del messaggio quando utilizzi l'interfaccia di chat.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Lingua da visualizzare",
|
||||
description:
|
||||
|
||||
@ -148,13 +148,6 @@ const TRANSLATIONS = {
|
||||
heading: "説明してください",
|
||||
body: "AnythingLLMの利点",
|
||||
},
|
||||
pfp: {
|
||||
title: "アシスタントのプロフィール画像",
|
||||
description:
|
||||
"このワークスペースのアシスタントのプロフィール画像をカスタマイズします。",
|
||||
image: "ワークスペース画像",
|
||||
remove: "ワークスペース画像を削除",
|
||||
},
|
||||
delete: {
|
||||
title: "ワークスペースを削除",
|
||||
description:
|
||||
@ -646,8 +639,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "メッセージを送信",
|
||||
attach_file: "このチャットにファイルを添付",
|
||||
slash: "チャットで使えるスラッシュコマンドをすべて表示",
|
||||
agents: "利用可能なエージェントをすべて表示",
|
||||
text_size: "テキストサイズを変更",
|
||||
microphone: "プロンプトを音声入力",
|
||||
send: "ワークスペースにプロンプトメッセージを送信",
|
||||
@ -660,18 +651,11 @@ const TRANSLATIONS = {
|
||||
good_response: "良い反応",
|
||||
more_actions:
|
||||
"さらに詳細な情報が必要な場合は、お気軽にお問い合わせください。",
|
||||
hide_citations: "参考文献を隠す",
|
||||
show_citations: "引用元を表示する",
|
||||
fork: "フォーク",
|
||||
delete: "削除",
|
||||
save_submit: "保存して送信",
|
||||
cancel: "キャンセル",
|
||||
edit_prompt: "編集のヒント",
|
||||
edit_response: "編集内容を保存します。",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "- このワークスペースのデフォルトエージェント。",
|
||||
custom_agents_coming_soon: "カスタムエージェントは近日公開予定です。",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"チャット履歴をクリアし、新しいチャットを開始してください。",
|
||||
add_new_preset: "新しいプリセットを追加する",
|
||||
@ -696,6 +680,35 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"認証情報を設定するには、ここをクリックしてください。",
|
||||
},
|
||||
submit: "送信",
|
||||
edit_info_user:
|
||||
"「送信」はAIの応答を再生成します。「保存」は、あなたのメッセージのみを更新します。",
|
||||
edit_info_assistant: "あなたの変更は、この回答に直接保存されます。",
|
||||
see_less: "詳細を見る",
|
||||
see_more: "詳細を見る",
|
||||
tools: "道具",
|
||||
browse: "閲覧",
|
||||
text_size_label: "文字サイズ",
|
||||
select_model: "モデルを選択",
|
||||
sources: "出典",
|
||||
document: "文書",
|
||||
similarity_match: "試合",
|
||||
source_count_one: "{{count}} 参照",
|
||||
source_count_other: "{{count}} への参照",
|
||||
preset_exit_description: "現在のエージェントセッションを停止する",
|
||||
add_new: "新しいものを追加する",
|
||||
edit: "編集",
|
||||
publish: "出版",
|
||||
stop_generating: "応答の生成を停止する",
|
||||
pause_tts_speech_message: "メッセージのテキスト読み上げ機能を一時停止する",
|
||||
slash_commands: "スラッシュコマンド",
|
||||
agent_skills: "エージェントのスキル",
|
||||
manage_agent_skills: "エージェントのスキル管理",
|
||||
agent_skills_disabled_in_session:
|
||||
"アクティブなセッション中にスキルを変更することはできません。まず、`/exit`コマンドを使用してセッションを終了してください。",
|
||||
start_agent_session: "エージェントセッションを開始",
|
||||
use_agent_session_to_use_tools:
|
||||
"チャットでツールを使用するには、プロンプトの冒頭に'@agent'を使用してエージェントセッションを開始してください。",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "アカウントを編集",
|
||||
@ -764,11 +777,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"ログインページに表示される名前を、すべてのユーザーに設定する。",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "チャットメッセージの整合性を確認する",
|
||||
description:
|
||||
"チャットインターフェースを使用する場合、メッセージの配置モードを選択してください。",
|
||||
},
|
||||
"display-language": {
|
||||
title: "表示言語",
|
||||
description:
|
||||
|
||||
@ -153,12 +153,6 @@ const TRANSLATIONS = {
|
||||
heading: "저에게 설명해주세요",
|
||||
body: "AnythingLLM의 장점",
|
||||
},
|
||||
pfp: {
|
||||
title: "어시스턴트 프로필 이미지",
|
||||
description: "이 워크스페이스의 어시스턴트 프로필 이미지를 수정합니다.",
|
||||
image: "워크스페이스 이미지",
|
||||
remove: "워크스페이스 이미지 제거",
|
||||
},
|
||||
delete: {
|
||||
title: "워크스페이스 삭제",
|
||||
description:
|
||||
@ -383,10 +377,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"로그인 페이지에 모든 사용자에게 표시될 애플리케이션 이름을 설정하세요.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "채팅 메시지 정렬",
|
||||
description: "채팅 인터페이스에서 메시지 정렬 방식을 선택하세요.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "표시 언어",
|
||||
description:
|
||||
@ -751,8 +741,6 @@ const TRANSLATIONS = {
|
||||
"첨부 파일을 처리 중입니다. 잠시만 기다려 주세요...",
|
||||
send_message: "메시지 보내기",
|
||||
attach_file: "이 채팅에 파일 첨부",
|
||||
slash: "채팅에서 사용할 수 있는 모든 슬래시 명령어 보기",
|
||||
agents: "채팅에 사용할 수 있는 모든 에이전트 보기",
|
||||
text_size: "텍스트 크기 변경",
|
||||
microphone: "프롬프트를 음성으로 입력",
|
||||
send: "프롬프트 메시지를 워크스페이스로 전송",
|
||||
@ -762,18 +750,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "응답 다시 생성",
|
||||
good_response: "좋은 답변",
|
||||
more_actions: "더 많은 작업",
|
||||
hide_citations: "인용 숨기기",
|
||||
show_citations: "인용 보기",
|
||||
fork: "포크",
|
||||
delete: "삭제",
|
||||
save_submit: "저장 및 제출",
|
||||
cancel: "취소",
|
||||
edit_prompt: "프롬프트 수정",
|
||||
edit_response: "응답 수정",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - 이 워크스페이스의 기본 에이전트입니다.",
|
||||
custom_agents_coming_soon: "커스텀 에이전트 기능이 곧 제공됩니다!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "채팅 기록을 초기화하고 새 채팅을 시작합니다",
|
||||
add_new_preset: "새 프리셋 추가",
|
||||
command: "명령어",
|
||||
@ -796,6 +777,35 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "이 제공자의 인증 정보가 없습니다!",
|
||||
missing_credentials_description: "클릭하여 인증 정보를 설정하세요",
|
||||
},
|
||||
submit: "제출",
|
||||
edit_info_user:
|
||||
'"제출"은 AI 응답을 다시 생성합니다. "저장"은 사용자 메시지만 업데이트합니다.',
|
||||
edit_info_assistant: "당신이 변경한 내용은 바로 이 답변에 저장됩니다.",
|
||||
see_less: "더 보기",
|
||||
see_more: "더 보기",
|
||||
tools: "도구",
|
||||
browse: "검색",
|
||||
text_size_label: "글자 크기",
|
||||
select_model: "모델 선택",
|
||||
sources: "출처",
|
||||
document: "문서",
|
||||
similarity_match: "경쟁",
|
||||
source_count_one: "{{count}} 참조",
|
||||
source_count_other: "{{count}} 관련 참고 자료",
|
||||
preset_exit_description: "현재 에이전트 세션을 중단",
|
||||
add_new: "새로운 항목 추가",
|
||||
edit: "수정",
|
||||
publish: "출판",
|
||||
stop_generating: "응답 생성 중단",
|
||||
pause_tts_speech_message: "메시지의 텍스트 음성 변환(TTS) 기능을 일시 중지",
|
||||
slash_commands: "슬래시 명령어",
|
||||
agent_skills: "에이전트의 역량",
|
||||
manage_agent_skills: "에이전트 역량 관리",
|
||||
agent_skills_disabled_in_session:
|
||||
"활성 에이전트 세션 중에 기술을 변경할 수 없습니다. 먼저 /exit 명령을 사용하여 세션을 종료하십시오.",
|
||||
start_agent_session: "에이전트 세션 시작",
|
||||
use_agent_session_to_use_tools:
|
||||
"채팅에서 도구를 사용하려면, 프롬프트의 시작 부분에 '@agent'을 사용하여 에이전트 세션을 시작할 수 있습니다.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "계정 정보 수정",
|
||||
|
||||
@ -156,12 +156,6 @@ const TRANSLATIONS = {
|
||||
heading: "Izskaidro man",
|
||||
body: "AnythingLLM priekšrocības",
|
||||
},
|
||||
pfp: {
|
||||
title: "Asistenta profila attēls",
|
||||
description: "Pielāgojiet asistenta profila attēlu šai darba telpai.",
|
||||
image: "Darba telpas attēls",
|
||||
remove: "Noņemt darba telpas attēlu",
|
||||
},
|
||||
delete: {
|
||||
title: "Dzēst darba telpu",
|
||||
description:
|
||||
@ -388,11 +382,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Iestatiet nosaukumu, kas tiek rādīts pieteikšanās lapā visiem lietotājiem.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Sarunas ziņu līdzinājums",
|
||||
description:
|
||||
"Izvēlieties ziņu līdzinājuma režīmu, izmantojot sarunas saskarni.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Displeja valoda",
|
||||
description:
|
||||
@ -765,8 +754,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Sūtīt ziņojumu",
|
||||
attach_file: "Pievienot failu šim čatam",
|
||||
slash: "Skatīt visas pieejamās slīpsvītras komandas čatošanai.",
|
||||
agents: "Skatīt visus pieejamos aģentus, kurus varat izmantot čatošanai.",
|
||||
text_size: "Mainīt teksta izmēru.",
|
||||
microphone: "Izrunājiet savu uzvedni.",
|
||||
send: "Nosūtīt uzvednes ziņojumu uz darba vietu",
|
||||
@ -777,19 +764,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Atjaunot atbildi",
|
||||
good_response: "Laba atbilde",
|
||||
more_actions: "Vairāk darbību",
|
||||
hide_citations: "Izvākt atsaukmes",
|
||||
show_citations: "Rādīt atsauces",
|
||||
fork: "Klūtis",
|
||||
delete: "Dzēst",
|
||||
save_submit: "Saglabāt un iesūt",
|
||||
cancel: "Atcelt",
|
||||
edit_prompt: "Ieslēgt",
|
||||
edit_response: "Rediģēt atbildi",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: "- noklusējuma aģents šim darba telpai.",
|
||||
custom_agents_coming_soon:
|
||||
"Nedaudz drīzumā būs pieejami individuāli pakalpojumi!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Izdzēsiet savu pastā veidoتو sarunu vēsturi un sāciet jaunu sarunu.",
|
||||
add_new_preset: "Pievienot jaunu iepriekšējo",
|
||||
@ -816,6 +795,37 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Noklikšķiniet, lai konfigurētu autentifikācijas datus",
|
||||
},
|
||||
submit: "Iesniegt",
|
||||
edit_info_user:
|
||||
'"Sūtīt" atjauno AI atbildi. "Saglabāt" atjauno tikai jūsu ziņu.',
|
||||
edit_info_assistant:
|
||||
"Jūsu izmaiņas tiks automātiski saglabātas šajā atbildē.",
|
||||
see_less: "Skatīt mazāk",
|
||||
see_more: "Skatīt vairāk",
|
||||
tools: "Rīki",
|
||||
browse: "Izpētiet",
|
||||
text_size_label: "Teksta izmērs",
|
||||
select_model: "Izvēlieties modeli",
|
||||
sources: "Avotus",
|
||||
document: "Dokuments",
|
||||
similarity_match: "spēle",
|
||||
source_count_one: "{{count}} – atsauce",
|
||||
source_count_other: "Atsauces uz {{count}}",
|
||||
preset_exit_description: "Aizust klientu sesiju",
|
||||
add_new: "Pievienot jaunu",
|
||||
edit: "Rediģēt",
|
||||
publish: "Publicēt",
|
||||
stop_generating: "Atsauciet atbildes ģenerēšanu",
|
||||
pause_tts_speech_message:
|
||||
"Pārtrauciet TTS (teksta-izrunas) žēstā vēstījuma izrunu.",
|
||||
slash_commands: "Īs termini komandās",
|
||||
agent_skills: "Aģenta prasmes",
|
||||
manage_agent_skills: "Iesaista aģenta prasmes",
|
||||
agent_skills_disabled_in_session:
|
||||
"Nav iespējams mainīt prasmes aktīvā lietotāja sesijā. Pirmais, jāizmanto komandu `/exit`, lai beigtu sesiju.",
|
||||
start_agent_session: "Sākt aģenta sesiju",
|
||||
use_agent_session_to_use_tools:
|
||||
'Jūs varat izmantot rīkus čatā, sākot aģenta sesiju, ievietojot "@agent" jūsu iniciālajā tekstā.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Rediģēt kontu",
|
||||
|
||||
@ -149,13 +149,6 @@ const TRANSLATIONS = {
|
||||
heading: "Leg me uit",
|
||||
body: "de voordelen van AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Assistent Profielfoto",
|
||||
description:
|
||||
"Pas de profielfoto van de assistent voor deze werkruimte aan.",
|
||||
image: "Werkruimte Afbeelding",
|
||||
remove: "Werkruimte Afbeelding Verwijderen",
|
||||
},
|
||||
delete: {
|
||||
title: "Werkruimte Verwijderen",
|
||||
description:
|
||||
@ -656,9 +649,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Een bericht verzenden",
|
||||
attach_file: "Een bestand aan deze chat toevoegen",
|
||||
slash: "Alle beschikbare slash-opdrachten voor chatten bekijken.",
|
||||
agents:
|
||||
"Alle beschikbare agents bekijken die je kunt gebruiken om te chatten.",
|
||||
text_size: "Tekstgrootte wijzigen.",
|
||||
microphone: "Spreek je prompt uit.",
|
||||
send: "Promptbericht naar werkruimte verzenden",
|
||||
@ -670,18 +660,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Reactie opnieuw genereren",
|
||||
good_response: "Goede reactie",
|
||||
more_actions: "Meer acties",
|
||||
hide_citations: "Citaten verbergen",
|
||||
show_citations: "Citaten weergeven",
|
||||
fork: "Fork",
|
||||
delete: "Verwijderen",
|
||||
save_submit: "Opslaan en verzenden",
|
||||
cancel: "Annuleren",
|
||||
edit_prompt: "Prompt bewerken",
|
||||
edit_response: "Reactie bewerken",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - de standaardagent voor deze werkruimte.",
|
||||
custom_agents_coming_soon: "Aangepaste agenten komen binnenkort!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Wis je chatgeschiedenis en begin een nieuwe chat",
|
||||
add_new_preset: "Nieuwe preset toevoegen",
|
||||
@ -704,6 +687,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Deze aanbieder mist logingegevens!",
|
||||
missing_credentials_description: "Klik om logingegevens in te stellen",
|
||||
},
|
||||
submit: "Indienen",
|
||||
edit_info_user:
|
||||
'"Verzenden" herstelt het antwoord van de AI. "Opslaan" wijzigt alleen uw bericht.',
|
||||
edit_info_assistant:
|
||||
"Uw wijzigingen worden direct op deze reactie opgeslagen.",
|
||||
see_less: "Minder zien",
|
||||
see_more: "Meer zien",
|
||||
tools: "Gereedschap",
|
||||
browse: "Bladeren",
|
||||
text_size_label: "Lettergrootte",
|
||||
select_model: "Kies het model",
|
||||
sources: "Bronnen",
|
||||
document: "Document",
|
||||
similarity_match: "wedstrijd",
|
||||
source_count_one: "{{count}} verwijzing",
|
||||
source_count_other: "{{count}} referenties",
|
||||
preset_exit_description: "Beëindig de huidige agent-sessie",
|
||||
add_new: "Voeg toe",
|
||||
edit: "Bewerk",
|
||||
publish: "Publiceren",
|
||||
stop_generating: "Stoppen met het genereren van antwoorden",
|
||||
pause_tts_speech_message: "Pauzeer de spraak van de tekstberichten.",
|
||||
slash_commands: "Korte commando's",
|
||||
agent_skills: "Vaardigheden van agenten",
|
||||
manage_agent_skills: "Beheer van de vaardigheden van de agent",
|
||||
agent_skills_disabled_in_session:
|
||||
"Het is niet mogelijk om vaardigheden aan te passen tijdens een actieve sessie. Gebruik eerst de commando `/exit` om de sessie te beëindigen.",
|
||||
start_agent_session: "Start Agent Sessie",
|
||||
use_agent_session_to_use_tools:
|
||||
'U kunt tools in de chat gebruiken door een sessie met een agent te starten, beginnend met "@agent" aan het begin van uw bericht.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Account bewerken",
|
||||
@ -772,11 +785,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Stel een naam in die op de inlogpagina voor alle gebruikers wordt weergegeven.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Uitlijning van chatberichten",
|
||||
description:
|
||||
"Selecteer de uitlijningsmodus voor berichten bij gebruik van de chatinterface.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Weergavetaal",
|
||||
description:
|
||||
|
||||
@ -156,12 +156,6 @@ const TRANSLATIONS = {
|
||||
heading: "Wyjaśnij mi",
|
||||
body: "Korzyści z AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Logo obszaru roboczego",
|
||||
description: "Dostosuj logo asystenta dla tego obszaru roboczego.",
|
||||
image: "Logo obszaru roboczego",
|
||||
remove: "Usuń logo obszaru roboczego",
|
||||
},
|
||||
delete: {
|
||||
title: "Usuń obszar roboczy",
|
||||
description:
|
||||
@ -390,11 +384,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Ustawienie nazwy wyświetlanej na stronie logowania dla wszystkich użytkowników.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Wyrównanie wiadomości czatu",
|
||||
description:
|
||||
"Wybór trybu wyrównania wiadomości podczas korzystania z interfejsu czatu.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Język",
|
||||
description:
|
||||
@ -767,8 +756,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Załączniki są przetwarzane. Proszę czekać...",
|
||||
send_message: "Wyślij wiadomość",
|
||||
attach_file: "Dołącz plik do tego czatu",
|
||||
slash: "Wyświetl wszystkie dostępne polecenia slash do czatowania.",
|
||||
agents: "Wyświetl wszystkich dostępnych agentów.",
|
||||
text_size: "Zmiana rozmiaru tekstu.",
|
||||
microphone: "Wypowiedz swoją prośbę.",
|
||||
send: "Wyślij wiadomość do obszaru roboczego",
|
||||
@ -778,18 +765,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Wygeneruj ponownie odpowiedź",
|
||||
good_response: "Dobra odpowiedź",
|
||||
more_actions: "Więcej działań",
|
||||
hide_citations: "Ukryj cytaty",
|
||||
show_citations: "Pokaż cytaty",
|
||||
fork: "Utwórz rozgałęzienie",
|
||||
delete: "Usuń",
|
||||
save_submit: "Zapisz i prześlij",
|
||||
cancel: "Anuluj",
|
||||
edit_prompt: "Edytuj prompt",
|
||||
edit_response: "Edytuj odpowiedź",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - domyślny agent dla tego obszaru roboczego.",
|
||||
custom_agents_coming_soon: "niestandardowi agenci już wkrótce!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Wyczyść historię czatu i rozpocznij nowy czat",
|
||||
add_new_preset: " Dodaj nowe polecenie slash",
|
||||
command: "Polecenie",
|
||||
@ -813,6 +793,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Kliknij, aby skonfigurować poświadczenia",
|
||||
},
|
||||
submit: "Prześlij",
|
||||
edit_info_user:
|
||||
'"Wyślij" powoduje ponowne wygenerowanie odpowiedzi przez sztuczną inteligencję. "Zapisz" aktualizuje tylko Twoje wiadomości.',
|
||||
edit_info_assistant:
|
||||
"Twoje zmiany zostaną zapisane bezpośrednio w tej odpowiedzi.",
|
||||
see_less: "Zobacz mniej",
|
||||
see_more: "Zobacz więcej",
|
||||
tools: "Narzędzia",
|
||||
browse: "Przeglądaj",
|
||||
text_size_label: "Rozmiar czcionki",
|
||||
select_model: "Wybierz model",
|
||||
sources: "Źródła",
|
||||
document: "Dokument",
|
||||
similarity_match: "mecz",
|
||||
source_count_one: "{{count}} – odniesienie",
|
||||
source_count_other: "{{count}} – odnośniki",
|
||||
preset_exit_description: "Zakończ bieżącą sesję z przedstawicielem",
|
||||
add_new: "Dodaj nowe",
|
||||
edit: "Edytuj",
|
||||
publish: "Opublikować",
|
||||
stop_generating: "Przestań generować odpowiedź",
|
||||
pause_tts_speech_message: "Wstrzymać odtwarzanie mowy z wiadomości",
|
||||
slash_commands: "Polecenia skrótowe",
|
||||
agent_skills: "Umiejętności agenta",
|
||||
manage_agent_skills: "Zarządzanie umiejętnościami agentów",
|
||||
agent_skills_disabled_in_session:
|
||||
"Nie można modyfikować umiejętności podczas trwającej sesji. Aby zakończyć sesję, należy najpierw użyć komendy /exit.",
|
||||
start_agent_session: "Rozpocznij sesję dla agenta",
|
||||
use_agent_session_to_use_tools:
|
||||
"Możesz korzystać z narzędzi w czacie, inicjując sesję z agentem, wpisując '@agent' na początku swojego zapytania.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Edytuj konto",
|
||||
|
||||
@ -156,12 +156,6 @@ const TRANSLATIONS = {
|
||||
heading: "Explique para mim",
|
||||
body: "os benefícios do AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Imagem do Assistente",
|
||||
description: "Personalize a imagem do assistente para este workspace.",
|
||||
image: "Imagem do Workspace",
|
||||
remove: "Remover Imagem",
|
||||
},
|
||||
delete: {
|
||||
title: "Excluir Workspace",
|
||||
description:
|
||||
@ -384,10 +378,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Defina um nome exibido na página de login para todos os usuários.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Alinhamento de Mensagens",
|
||||
description: "Selecione o alinhamento das mensagens no chat.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Idioma",
|
||||
description:
|
||||
@ -748,8 +738,6 @@ const TRANSLATIONS = {
|
||||
attachments_processing: "Anexos em processamento. Aguarde...",
|
||||
send_message: "Enviar mensagem",
|
||||
attach_file: "Anexar arquivo ao chat",
|
||||
slash: "Veja todos os comandos disponíveis.",
|
||||
agents: "Veja todos os agentes disponíveis.",
|
||||
text_size: "Alterar tamanho do texto.",
|
||||
microphone: "Fale seu prompt.",
|
||||
send: "Enviar prompt para o workspace",
|
||||
@ -759,18 +747,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Regerar resposta",
|
||||
good_response: "Resposta satisfatória",
|
||||
more_actions: "Mais ações",
|
||||
hide_citations: "Esconder citações",
|
||||
show_citations: "Exibir citações",
|
||||
fork: "Fork",
|
||||
delete: "Excluir",
|
||||
save_submit: "Alterar",
|
||||
cancel: "Cancelar",
|
||||
edit_prompt: "Editar prompt",
|
||||
edit_response: "Editar resposta",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - o agente padrão deste workspace.",
|
||||
custom_agents_coming_soon: "mais agentes personalizados em breve!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Limpa o histórico do seu chat e inicia um novo",
|
||||
add_new_preset: " Insere um novo Preset",
|
||||
command: "Comando",
|
||||
@ -794,6 +775,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Configure as credenciais do LLM primeiro",
|
||||
},
|
||||
submit: "Enviar",
|
||||
edit_info_user:
|
||||
'"Enviar" recria a resposta da IA. "Salvar" atualiza apenas sua mensagem.',
|
||||
edit_info_assistant:
|
||||
"Suas alterações serão salvas diretamente nesta resposta.",
|
||||
see_less: "Ver menos",
|
||||
see_more: "Ver mais",
|
||||
tools: "Ferramentas",
|
||||
browse: "Navegar",
|
||||
text_size_label: "Tamanho do texto",
|
||||
select_model: "Selecione o modelo",
|
||||
sources: "Fontes",
|
||||
document: "Documento",
|
||||
similarity_match: "jogo",
|
||||
source_count_one: "Referência a {{count}}",
|
||||
source_count_other: "Referências a {{count}}",
|
||||
preset_exit_description: "Interrompa a sessão atual do agente",
|
||||
add_new: "Adicionar novo",
|
||||
edit: "Editar",
|
||||
publish: "Publicar",
|
||||
stop_generating: "Pare de gerar respostas",
|
||||
pause_tts_speech_message: "Pausar a leitura de voz da mensagem",
|
||||
slash_commands: "Comandos Rápidos",
|
||||
agent_skills: "Habilidades do Agente",
|
||||
manage_agent_skills: "Gerenciar as habilidades dos agentes",
|
||||
agent_skills_disabled_in_session:
|
||||
"Não é possível modificar as habilidades durante uma sessão de agente ativa. Utilize o comando `/exit` para encerrar a sessão primeiro.",
|
||||
start_agent_session: "Iniciar Sessão de Agente",
|
||||
use_agent_session_to_use_tools:
|
||||
'Você pode utilizar as ferramentas disponíveis no chat iniciando uma sessão com um agente, adicionando "@agent" no início da sua mensagem.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editar conta",
|
||||
|
||||
@ -158,13 +158,6 @@ const TRANSLATIONS = {
|
||||
heading: "Explică-mi",
|
||||
body: "beneficiile AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Imagine profil asistent",
|
||||
description:
|
||||
"Personalizează imaginea de profil a asistentului pentru acest spațiu de lucru.",
|
||||
image: "Imagine spațiu de lucru",
|
||||
remove: "Șterge imaginea spațiului de lucru",
|
||||
},
|
||||
delete: {
|
||||
title: "Șterge spațiul de lucru",
|
||||
description:
|
||||
@ -498,8 +491,6 @@ const TRANSLATIONS = {
|
||||
"Fișierele atașate se procesează. Te rugăm să aștepți...",
|
||||
send_message: "Trimite mesaj",
|
||||
attach_file: "Atașează un fișier la acest chat",
|
||||
slash: "Vizualizează toate comenzile slash disponibile pentru chat.",
|
||||
agents: "Vezi toți agenții disponibili pentru chat.",
|
||||
text_size: "Schimbă dimensiunea textului.",
|
||||
microphone: "Vorbește promptul tău.",
|
||||
send: "Trimite prompt către spațiul de lucru",
|
||||
@ -509,19 +500,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Regenerare răspuns",
|
||||
good_response: "Răspuns bun",
|
||||
more_actions: "Mai multe acțiuni",
|
||||
hide_citations: "Ascunde citările",
|
||||
show_citations: "Arată citările",
|
||||
fork: "Fork",
|
||||
delete: "Șterge",
|
||||
save_submit: "Salvează & Trimite",
|
||||
cancel: "Anulează",
|
||||
edit_prompt: "Editează prompt",
|
||||
edit_response: "Editează răspuns",
|
||||
at_agent: "@agent",
|
||||
default_agent_description:
|
||||
" - agentul implicit pentru acest spațiu de lucru.",
|
||||
custom_agents_coming_soon: "agenții personalizați vin în curând!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Șterge istoricul chatului și începe o conversație nouă",
|
||||
add_new_preset: " Adaugă preset nou",
|
||||
@ -546,6 +529,37 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Acest furnizor lipsește credențiale!",
|
||||
missing_credentials_description: "Click pentru a configura credențialele",
|
||||
},
|
||||
submit: "Trimite",
|
||||
edit_info_user:
|
||||
"„Trimite” recreează răspunsul generat de inteligența artificială. „Salvează” actualizează doar mesajul dumneavoastră.",
|
||||
edit_info_assistant:
|
||||
"Modificările pe care le faceți vor fi salvate direct în acest răspuns.",
|
||||
see_less: "Vezi mai puțin",
|
||||
see_more: "Vezi mai multe",
|
||||
tools: "Unelte",
|
||||
browse: "Navigați",
|
||||
text_size_label: "Dimensiunea textului",
|
||||
select_model: "Selectați modelul",
|
||||
sources: "Surse",
|
||||
document: "Document",
|
||||
similarity_match: "meci",
|
||||
source_count_one: "{{count}} – referință",
|
||||
source_count_other: "Referințe către {{count}}",
|
||||
preset_exit_description: "Întrerupeți sesiunea actuală a agentului",
|
||||
add_new: "Adaugă",
|
||||
edit: "Editează",
|
||||
publish: "Publica",
|
||||
stop_generating: "Opriți generarea răspunsului",
|
||||
pause_tts_speech_message:
|
||||
"Pauză în redarea vocii prin Text-to-Speech (TTS) a mesajului.",
|
||||
slash_commands: "Comenzi scurte",
|
||||
agent_skills: "Abilități ale agentului",
|
||||
manage_agent_skills: "Gestionarea competențelor agenților",
|
||||
agent_skills_disabled_in_session:
|
||||
"Nu este posibil să modificați abilitățile în timpul unei sesiuni cu un agent activ. Pentru a încheia sesiunea, utilizați comanda /exit.",
|
||||
start_agent_session: "Începe sesiunea de agent",
|
||||
use_agent_session_to_use_tools:
|
||||
'Puteți utiliza instrumentele disponibile în chat, inițiind o sesiune cu un agent, începând mesajul cu "@agent".',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editează contul",
|
||||
@ -815,11 +829,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Setează un nume care este afișat pe pagina de autentificare tuturor utilizatorilor.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Alinierea mesajelor de chat",
|
||||
description:
|
||||
"Selectează modul de aliniere a mesajelor când folosești interfața de chat.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Limba de afișare",
|
||||
description:
|
||||
|
||||
@ -149,13 +149,6 @@ const TRANSLATIONS = {
|
||||
heading: "Объясните мне",
|
||||
body: "преимущества AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Изображение профиля помощника",
|
||||
description:
|
||||
"Настройте изображение профиля помощника для этого рабочего пространства.",
|
||||
image: "Изображение рабочего пространства",
|
||||
remove: "Удалить изображение рабочего пространства",
|
||||
},
|
||||
delete: {
|
||||
title: "Удалить Рабочее Пространство",
|
||||
description:
|
||||
@ -654,8 +647,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Отправить сообщение",
|
||||
attach_file: "Прикрепить файл к чату",
|
||||
slash: "Просмотреть все доступные слэш-команды для чата.",
|
||||
agents: "Просмотреть всех доступных агентов для чата.",
|
||||
text_size: "Изменить размер текста.",
|
||||
microphone: "Произнесите ваш запрос.",
|
||||
send: "Отправить запрос в рабочее пространство",
|
||||
@ -666,20 +657,12 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Перефразировать ответ",
|
||||
good_response: "Хороший ответ",
|
||||
more_actions: "Больше действий",
|
||||
hide_citations: "Скрыть ссылки на источники",
|
||||
show_citations: "Отображение ссылок",
|
||||
fork: "Вилка",
|
||||
delete: "Удалить",
|
||||
save_submit: "Сохранить и отправить",
|
||||
cancel: "Отменить",
|
||||
edit_prompt:
|
||||
"Пожалуйста, предоставьте текст, который необходимо отредактировать.",
|
||||
edit_response: "Отредактируйте ответ",
|
||||
at_agent: "@agent",
|
||||
default_agent_description:
|
||||
"- это основной агент для данного рабочего пространства.",
|
||||
custom_agents_coming_soon: "Скоро появятся индивидуальные агенты!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "Очистите историю чата и начните новый чат",
|
||||
add_new_preset: "Добавить новый шаблон",
|
||||
command: "Команда",
|
||||
@ -707,6 +690,37 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Нажмите, чтобы настроить учетные данные",
|
||||
},
|
||||
submit: "Отправить",
|
||||
edit_info_user:
|
||||
'"Отправить" генерирует новый ответ от ИИ. "Сохранить" обновляет только ваше сообщение.',
|
||||
edit_info_assistant:
|
||||
"Ваши изменения будут сохранены непосредственно в этом ответе.",
|
||||
see_less: "Показать меньше",
|
||||
see_more: "Узнать больше",
|
||||
tools: "Инструменты",
|
||||
browse: "Просматривать",
|
||||
text_size_label: "Размер текста",
|
||||
select_model: "Выберите модель",
|
||||
sources: "Источники",
|
||||
document: "Документ",
|
||||
similarity_match: "соревнование; игра",
|
||||
source_count_one: "{{count}} – ссылка",
|
||||
source_count_other: "Ссылки на {{count}}",
|
||||
preset_exit_description: "Прекратить текущую сессию работы с агентом",
|
||||
add_new: "Добавить новое",
|
||||
edit: "Редактировать",
|
||||
publish: "Опубликовать",
|
||||
stop_generating: "Прекратите генерацию ответа",
|
||||
pause_tts_speech_message:
|
||||
"Приостановить чтение текста с помощью синтезатора речи.",
|
||||
slash_commands: "Команды, введенные сокращенной формой",
|
||||
agent_skills: "Навыки агента",
|
||||
manage_agent_skills: "Управление навыками агентов",
|
||||
agent_skills_disabled_in_session:
|
||||
"Невозможно изменять навыки во время активной сессии. Для завершения сессии сначала используйте команду /exit.",
|
||||
start_agent_session: "Начать сеанс для агента",
|
||||
use_agent_session_to_use_tools:
|
||||
"Вы можете использовать инструменты в чате, начав сеанс с агентом, добавив '@agent' в начало вашего сообщения.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Редактировать учётную запись",
|
||||
@ -777,11 +791,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Укажите имя, которое будет отображаться на странице входа для всех пользователей.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Выравнивание сообщений в чате",
|
||||
description:
|
||||
"Выберите режим выравнивания сообщений при использовании интерфейса чата.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Язык отображения",
|
||||
description:
|
||||
|
||||
@ -149,13 +149,6 @@ const TRANSLATIONS = {
|
||||
heading: "Bana açıkla",
|
||||
body: "AnythingLLM'nin faydalarını",
|
||||
},
|
||||
pfp: {
|
||||
title: "Asistan Profil Görseli",
|
||||
description:
|
||||
"Bu çalışma alanı için asistanın profil resmini özelleştirin.",
|
||||
image: "Çalışma Alanı Görseli",
|
||||
remove: "Çalışma Alanı Görselini Kaldır",
|
||||
},
|
||||
delete: {
|
||||
title: "Çalışma Alanını Sil",
|
||||
description:
|
||||
@ -653,8 +646,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Mesaj gönderin",
|
||||
attach_file: "Bu sohbete bir dosya ekleyin",
|
||||
slash: "Sohbet için mevcut tüm eğik çizgi komutlarını görüntüleyin.",
|
||||
agents: "Sohbet için kullanabileceğiniz tüm ajanları görüntüleyin.",
|
||||
text_size: "Metin boyutunu değiştirin.",
|
||||
microphone: "Promptunuzu söyleyin.",
|
||||
send: "Çalışma alanına prompt mesajı gönderin",
|
||||
@ -665,18 +656,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Yanıtı yeniden oluştur",
|
||||
good_response: "İyi yanıt",
|
||||
more_actions: "Daha fazla eylem",
|
||||
hide_citations: "Alıntıları gizle",
|
||||
show_citations: "Alıntıları göster",
|
||||
fork: "Çatalla",
|
||||
delete: "Sil",
|
||||
save_submit: "Kaydet & Gönder",
|
||||
cancel: "İptal",
|
||||
edit_prompt: "Promptu düzenle",
|
||||
edit_response: "Yanıtı düzenle",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - bu çalışma alanının varsayılan ajanı.",
|
||||
custom_agents_coming_soon: "özel ajanlar yakında!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Sohbet geçmişinizi temizleyin ve yeni bir sohbet başlatın",
|
||||
add_new_preset: " Yeni Ön Ayar Ekle",
|
||||
@ -701,6 +685,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials_description:
|
||||
"Kimlik bilgilerini ayarlamak için tıklayın",
|
||||
},
|
||||
submit: "Gönder",
|
||||
edit_info_user:
|
||||
'"Gönder" seçeneği, yapay zeka yanıtını yeniden oluşturur. "Kaydet" seçeneği, yalnızca sizin mesajınızı günceller.',
|
||||
edit_info_assistant:
|
||||
"Yaptığınız değişiklikler doğrudan bu yanıtın içine kaydedilecektir.",
|
||||
see_less: "Daha az",
|
||||
see_more: "Daha Fazla",
|
||||
tools: "Araçlar",
|
||||
browse: "Gezin",
|
||||
text_size_label: "Metin Boyutu",
|
||||
select_model: "Model Seçimi",
|
||||
sources: "Kaynaklar",
|
||||
document: "Belge",
|
||||
similarity_match: "maç",
|
||||
source_count_one: "{{count}} ile ilgili bilgi",
|
||||
source_count_other: "{{count}} referansları",
|
||||
preset_exit_description: "Mevcut ajan oturumunu durdurun",
|
||||
add_new: "Yeni ekle",
|
||||
edit: "Düzenle",
|
||||
publish: "Yayınla",
|
||||
stop_generating: "Yanıt üretmeyi durdurun",
|
||||
pause_tts_speech_message: "Mesajın metin okuma (TTS) özelliğini durdur",
|
||||
slash_commands: "Komut Satırı Komutları",
|
||||
agent_skills: "Ajansın Yetenekleri",
|
||||
manage_agent_skills: "Temsilcinin becerilerini yönetin",
|
||||
agent_skills_disabled_in_session:
|
||||
"Aktif bir ajan oturumunda becerileri değiştirilemez. İlk olarak /exit komutunu kullanarak oturumu sonlandırın.",
|
||||
start_agent_session: "Temsilci Oturumu Başlat",
|
||||
use_agent_session_to_use_tools:
|
||||
'Çatınızdaki araçları kullanmak için, isteminizin başında "@agent" ile bir ajan oturumu başlatabilirsiniz.',
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Hesabı Düzenle",
|
||||
@ -770,11 +784,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Giriş sayfasında tüm kullanıcılara gösterilen bir ad ayarlayın.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Sohbet Mesajı Hizalaması",
|
||||
description:
|
||||
"Sohbet arayüzünü kullanırken mesaj hizalama modunu seçin.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Görüntüleme Dili",
|
||||
description:
|
||||
|
||||
@ -149,13 +149,6 @@ const TRANSLATIONS = {
|
||||
heading: "Giải thích cho tôi",
|
||||
body: "các lợi ích của AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Hình đại diện trợ lý",
|
||||
description:
|
||||
"Tùy chỉnh hình ảnh hồ sơ của trợ lý cho không gian làm việc này.",
|
||||
image: "Hình ảnh Không gian làm việc",
|
||||
remove: "Xóa Hình ảnh Không gian làm việc",
|
||||
},
|
||||
delete: {
|
||||
title: "Xóa không gian làm việc",
|
||||
description:
|
||||
@ -651,8 +644,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "Gửi tin nhắn",
|
||||
attach_file: "Đính kèm tệp vào cuộc trò chuyện này",
|
||||
slash: "Xem tất cả các lệnh gạch chéo có sẵn để trò chuyện.",
|
||||
agents: "Xem tất cả các agent có sẵn bạn có thể sử dụng để trò chuyện.",
|
||||
text_size: "Thay đổi kích thước văn bản.",
|
||||
microphone: "Nói prompt của bạn.",
|
||||
send: "Gửi tin nhắn prompt đến không gian làm việc",
|
||||
@ -663,18 +654,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "Tạo lại phản hồi",
|
||||
good_response: "Phản hồi tốt",
|
||||
more_actions: "Thêm hành động",
|
||||
hide_citations: "Ẩn trích dẫn",
|
||||
show_citations: "Hiện trích dẫn",
|
||||
fork: "Rẽ nhánh",
|
||||
delete: "Xóa",
|
||||
save_submit: "Lưu & Gửi",
|
||||
cancel: "Hủy",
|
||||
edit_prompt: "Chỉnh sửa prompt",
|
||||
edit_response: "Chỉnh sửa phản hồi",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - agent mặc định cho không gian làm việc này.",
|
||||
custom_agents_coming_soon: "agent tùy chỉnh sắp ra mắt!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description:
|
||||
"Xóa lịch sử trò chuyện và bắt đầu cuộc trò chuyện mới",
|
||||
add_new_preset: " Thêm Cài đặt sẵn Mới",
|
||||
@ -698,6 +682,36 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Nhà cung cấp này thiếu thông tin đăng nhập!",
|
||||
missing_credentials_description: "Nhấp để thiết lập thông tin đăng nhập",
|
||||
},
|
||||
submit: "Gửi",
|
||||
edit_info_user:
|
||||
'"Gửi" sẽ tạo lại phản hồi của AI. "Lưu" chỉ cập nhật tin nhắn của bạn.',
|
||||
edit_info_assistant:
|
||||
"Các thay đổi của bạn sẽ được lưu trực tiếp vào phản hồi này.",
|
||||
see_less: "Xem ít hơn",
|
||||
see_more: "Xem thêm",
|
||||
tools: "Dụng cụ",
|
||||
browse: "Duyệt",
|
||||
text_size_label: "Kích thước văn bản",
|
||||
select_model: "Chọn mẫu",
|
||||
sources: "Nguồn",
|
||||
document: "Tài liệu",
|
||||
similarity_match: "trận đấu",
|
||||
source_count_one: "{{count}} tham khảo",
|
||||
source_count_other: "{{count}} – Tham khảo",
|
||||
preset_exit_description: "Dừng lại phiên làm việc hiện tại",
|
||||
add_new: "Thêm mới",
|
||||
edit: "Chỉnh sửa",
|
||||
publish: "Đăng tải",
|
||||
stop_generating: "Dừng tạo ra phản hồi",
|
||||
pause_tts_speech_message: "Tạm dừng phát giọng đọc của tin nhắn",
|
||||
slash_commands: "Lệnh tắt/bật",
|
||||
agent_skills: "Kỹ năng của đại lý",
|
||||
manage_agent_skills: "Quản lý kỹ năng của đại lý",
|
||||
agent_skills_disabled_in_session:
|
||||
"Không thể thay đổi kỹ năng trong khi đang tham gia phiên làm việc. Trước tiên, hãy sử dụng lệnh /exit để kết thúc phiên làm việc.",
|
||||
start_agent_session: "Bắt đầu phiên làm việc với đại lý",
|
||||
use_agent_session_to_use_tools:
|
||||
"Bạn có thể sử dụng các công cụ trong cuộc trò chuyện bằng cách bắt đầu một phiên với trợ lý bằng cách sử dụng '@agent' ở đầu yêu cầu của bạn.",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Chỉnh sửa Tài khoản",
|
||||
@ -766,11 +780,6 @@ const TRANSLATIONS = {
|
||||
description:
|
||||
"Đặt tên được hiển thị trên trang đăng nhập cho tất cả người dùng.",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "Căn chỉnh Tin nhắn Trò chuyện",
|
||||
description:
|
||||
"Chọn chế độ căn chỉnh tin nhắn khi sử dụng giao diện trò chuyện.",
|
||||
},
|
||||
"display-language": {
|
||||
title: "Ngôn ngữ Hiển thị",
|
||||
description:
|
||||
|
||||
@ -151,12 +151,6 @@ const TRANSLATIONS = {
|
||||
heading: "向我解释",
|
||||
body: "AnythingLLM 的好处",
|
||||
},
|
||||
pfp: {
|
||||
title: "助理头像",
|
||||
description: "为此工作区自定义助手的个人资料图像。",
|
||||
image: "工作区图像",
|
||||
remove: "移除工作区图像",
|
||||
},
|
||||
delete: {
|
||||
title: "删除工作区",
|
||||
description: "删除此工作区及其所有数据。这将删除所有用户的工作区。",
|
||||
@ -368,10 +362,6 @@ const TRANSLATIONS = {
|
||||
title: "名称",
|
||||
description: "设置所有用户在登录页面看到的名称。",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "聊天消息对齐方式",
|
||||
description: "选择在聊天界面中使用的消息对齐模式。",
|
||||
},
|
||||
"display-language": {
|
||||
title: "显示语言",
|
||||
description: "选择显示 AnythingLLM 界面所用的语言(若有翻译可用)。",
|
||||
@ -713,8 +703,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "发送消息",
|
||||
attach_file: "向此对话附加文件",
|
||||
slash: "查看所有可用的聊天斜杠命令。",
|
||||
agents: "查看所有可用的聊天助手。",
|
||||
text_size: "更改文字大小。",
|
||||
microphone: "语音输入你的提示。",
|
||||
send: "将提示消息发送到工作区",
|
||||
@ -725,18 +713,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "重新回应",
|
||||
good_response: "反应良好",
|
||||
more_actions: "更多操作",
|
||||
hide_citations: "隐藏引文",
|
||||
show_citations: "显示引文",
|
||||
fork: "分叉",
|
||||
delete: "删除",
|
||||
save_submit: "提交保存",
|
||||
cancel: "取消",
|
||||
edit_prompt: "编辑问题",
|
||||
edit_response: "编辑回应",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - 此工作区的预设代理。",
|
||||
custom_agents_coming_soon: "自定义代理功能即将推出!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "清除聊天纪录并开始新的聊天",
|
||||
add_new_preset: "新增预设",
|
||||
command: "指令",
|
||||
@ -758,6 +739,34 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "缺少凭证",
|
||||
missing_credentials_description: "缺少凭证说明",
|
||||
},
|
||||
submit: "提交",
|
||||
edit_info_user: "“提交”会重新生成 AI 的回复。 “保存”只会更新您的消息。",
|
||||
edit_info_assistant: "您所做的修改将直接保存到此处。",
|
||||
see_less: "查看更多",
|
||||
see_more: "查看更多",
|
||||
tools: "工具",
|
||||
browse: "浏览",
|
||||
text_size_label: "字体大小",
|
||||
select_model: "选择型号",
|
||||
sources: "来源",
|
||||
document: "文件",
|
||||
similarity_match: "比赛",
|
||||
source_count_one: "{{count}} 参考",
|
||||
source_count_other: "{{count}} 相关资料",
|
||||
preset_exit_description: "停止当前的代理会话",
|
||||
add_new: "添加新",
|
||||
edit: "编辑",
|
||||
publish: "出版",
|
||||
stop_generating: "停止生成回复",
|
||||
pause_tts_speech_message: "暂停消息的语音合成(TTS)功能",
|
||||
slash_commands: "快捷命令",
|
||||
agent_skills: "代理人技能",
|
||||
manage_agent_skills: "管理代理人技能",
|
||||
agent_skills_disabled_in_session:
|
||||
"在活动会话期间,无法修改技能。首先使用 /exit 命令结束会话。",
|
||||
start_agent_session: "开始代理会",
|
||||
use_agent_session_to_use_tools:
|
||||
"您可以通过在提示词的开头使用'@agent'来启动与代理的聊天,从而使用聊天工具。",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "编辑帐户",
|
||||
|
||||
@ -143,12 +143,6 @@ const TRANSLATIONS = {
|
||||
heading: "請向我說明",
|
||||
body: "AnythingLLM 的優點",
|
||||
},
|
||||
pfp: {
|
||||
title: "助理個人檔案圖片",
|
||||
description: "自訂此工作區助理的個人檔案圖片。",
|
||||
image: "工作區圖片",
|
||||
remove: "移除工作區圖片",
|
||||
},
|
||||
delete: {
|
||||
title: "刪除工作區",
|
||||
description: "刪除此工作區及其所有資料。這將會為所有使用者刪除該工作區。",
|
||||
@ -613,8 +607,6 @@ const TRANSLATIONS = {
|
||||
chat_window: {
|
||||
send_message: "發送訊息",
|
||||
attach_file: "附加檔案到此對話",
|
||||
slash: "查看所有可用的斜線指令。",
|
||||
agents: "查看所有可用的聊天代理。",
|
||||
text_size: "變更文字大小。",
|
||||
microphone: "語音輸入提示。",
|
||||
send: "將提示訊息發送到工作區",
|
||||
@ -625,18 +617,11 @@ const TRANSLATIONS = {
|
||||
regenerate_response: "重新回應",
|
||||
good_response: "反應良好",
|
||||
more_actions: "更多操作",
|
||||
hide_citations: "隱藏引文",
|
||||
show_citations: "顯示引文",
|
||||
fork: "分叉",
|
||||
delete: "刪除",
|
||||
save_submit: "提交保存",
|
||||
cancel: "取消",
|
||||
edit_prompt: "編輯問題",
|
||||
edit_response: "編輯回應",
|
||||
at_agent: "@agent",
|
||||
default_agent_description: " - 此工作區的預設代理。",
|
||||
custom_agents_coming_soon: "自訂代理功能即將推出!",
|
||||
slash_reset: "/reset",
|
||||
preset_reset_description: "清除聊天紀錄並開始新的聊天",
|
||||
add_new_preset: "新增預設",
|
||||
command: "指令",
|
||||
@ -658,6 +643,34 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "缺少憑證",
|
||||
missing_credentials_description: "缺少憑證說明",
|
||||
},
|
||||
submit: "提交",
|
||||
edit_info_user: "「提交」會重新產生 AI 的回覆。 「儲存」僅會更新您的訊息。",
|
||||
edit_info_assistant: "您的修改將直接儲存到此處。",
|
||||
see_less: "查看更多",
|
||||
see_more: "查看更多",
|
||||
tools: "工具",
|
||||
browse: "瀏覽",
|
||||
text_size_label: "文字大小",
|
||||
select_model: "選擇模型",
|
||||
sources: "來源",
|
||||
document: "文件",
|
||||
similarity_match: "比賽",
|
||||
source_count_one: "{{count}} 參考",
|
||||
source_count_other: "{{count}} 的相關資料",
|
||||
preset_exit_description: "暫停目前的工作階段",
|
||||
add_new: "新增",
|
||||
edit: "編輯",
|
||||
publish: "發行",
|
||||
stop_generating: "停止生成回應",
|
||||
pause_tts_speech_message: "暫停語音合成的訊息",
|
||||
slash_commands: "簡短指令",
|
||||
agent_skills: "代理人技能",
|
||||
manage_agent_skills: "管理代理人技能",
|
||||
agent_skills_disabled_in_session:
|
||||
"在執行代理時,無法修改技能。請先使用 /exit 命令結束本次執行。",
|
||||
start_agent_session: "開始代理會談",
|
||||
use_agent_session_to_use_tools:
|
||||
"您可以使用聊天中的工具,只需在您的指令開頭加上'@agent',即可開始與代理的對話。",
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "編輯帳戶",
|
||||
@ -721,10 +734,6 @@ const TRANSLATIONS = {
|
||||
title: "應用名稱",
|
||||
description: "設定所有使用者在登入頁面上看到的應用名稱。",
|
||||
},
|
||||
"chat-message-alignment": {
|
||||
title: "聊天訊息對齊方式",
|
||||
description: "選擇使用聊天介面時訊息的對齊模式。",
|
||||
},
|
||||
"display-language": {
|
||||
title: "顯示語言",
|
||||
description: "選擇 AnythingLLM 使用者介面的顯示語言(如有提供翻譯)。",
|
||||
|
||||
@ -99,20 +99,16 @@ const Workspace = {
|
||||
return this.threads._deleteEditedChats(slug, threadSlug, startingId);
|
||||
return this._deleteEditedChats(slug, startingId);
|
||||
},
|
||||
updateChatResponse: async function (
|
||||
updateChat: async function (
|
||||
slug = "",
|
||||
threadSlug = "",
|
||||
chatId,
|
||||
newText
|
||||
newText,
|
||||
role = "assistant"
|
||||
) {
|
||||
if (!!threadSlug)
|
||||
return this.threads._updateChatResponse(
|
||||
slug,
|
||||
threadSlug,
|
||||
chatId,
|
||||
newText
|
||||
);
|
||||
return this._updateChatResponse(slug, chatId, newText);
|
||||
return this.threads._updateChat(slug, threadSlug, chatId, newText, role);
|
||||
return this._updateChat(slug, chatId, newText, role);
|
||||
},
|
||||
multiplexStream: async function ({
|
||||
workspaceSlug,
|
||||
@ -398,11 +394,11 @@ const Workspace = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
_updateChatResponse: async function (slug = "", chatId, newText) {
|
||||
_updateChat: async function (slug = "", chatId, newText, role = "assistant") {
|
||||
return await fetch(`${API_BASE}/workspace/${slug}/update-chat`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ chatId, newText }),
|
||||
body: JSON.stringify({ chatId, newText, role }),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) return true;
|
||||
|
||||
@ -184,18 +184,19 @@ const WorkspaceThread = {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
_updateChatResponse: async function (
|
||||
_updateChat: async function (
|
||||
workspaceSlug = "",
|
||||
threadSlug = "",
|
||||
chatId,
|
||||
newText
|
||||
newText,
|
||||
role = "assistant"
|
||||
) {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update-chat`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ chatId, newText }),
|
||||
body: JSON.stringify({ chatId, newText, role }),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
|
||||
@ -3,7 +3,6 @@ import { isMobile } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LanguagePreference from "../components/LanguagePreference";
|
||||
import ThemePreference from "../components/ThemePreference";
|
||||
import { MessageDirection } from "../components/MessageDirection";
|
||||
|
||||
export default function InterfaceSettings() {
|
||||
const { t } = useTranslation();
|
||||
@ -28,7 +27,6 @@ export default function InterfaceSettings() {
|
||||
</div>
|
||||
<ThemePreference />
|
||||
<LanguagePreference />
|
||||
<MessageDirection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -23,6 +23,8 @@ import { safeJsonParse } from "@/utils/request";
|
||||
import QuickActions from "@/components/lib/QuickActions";
|
||||
import SuggestedMessages from "@/components/lib/SuggestedMessages";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import TextSizeMenu from "@/components/WorkspaceChat/ChatContainer/TextSizeMenu";
|
||||
import WorkspaceModelPicker from "@/components/WorkspaceChat/ChatContainer/WorkspaceModelPicker";
|
||||
import { ChatTooltips } from "@/components/WorkspaceChat/ChatContainer/ChatTooltips";
|
||||
|
||||
async function getTargetWorkspace() {
|
||||
@ -129,7 +131,7 @@ export default function Home() {
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -242,6 +244,12 @@ function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
|
||||
writeMode = "replace",
|
||||
}) {
|
||||
if (autoSubmit) {
|
||||
if (writeMode === "append") {
|
||||
const currentText =
|
||||
document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
|
||||
text = currentText + text;
|
||||
}
|
||||
if (!text.trim()) return;
|
||||
submitMessage(text.trim());
|
||||
return;
|
||||
}
|
||||
@ -269,9 +277,11 @@ function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<TextSizeMenu />
|
||||
<WorkspaceModelPicker workspaceSlug={workspace?.slug} />
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center w-full max-w-[750px]">
|
||||
@ -312,7 +322,7 @@ function NoWorkspacesAssigned() {
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<p className="text-white/60 text-sm text-center whitespace-pre-line">
|
||||
|
||||
@ -13,7 +13,7 @@ export default function Main() {
|
||||
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
|
||||
{!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
|
||||
<Home />
|
||||
</div>
|
||||
|
||||
@ -31,11 +31,9 @@ function ShowWorkspaceChat() {
|
||||
if (!_workspace) return setLoading(false);
|
||||
|
||||
const suggestedMessages = await Workspace.getSuggestedMessages(slug);
|
||||
const pfpUrl = await Workspace.fetchPfp(slug);
|
||||
setWorkspace({
|
||||
..._workspace,
|
||||
suggestedMessages,
|
||||
pfpUrl,
|
||||
});
|
||||
setLoading(false);
|
||||
localStorage.setItem(
|
||||
@ -51,7 +49,7 @@ function ShowWorkspaceChat() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<WorkspaceChatContainer loading={loading} workspace={workspace} />
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
|
||||
import WorkspaceName from "./WorkspaceName";
|
||||
import SuggestedChatMessages from "./SuggestedChatMessages";
|
||||
import DeleteWorkspace from "./DeleteWorkspace";
|
||||
import WorkspacePfp from "./WorkspacePfp";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
|
||||
export default function GeneralInfo({ slug }) {
|
||||
@ -65,7 +64,6 @@ export default function GeneralInfo({ slug }) {
|
||||
/>
|
||||
</form>
|
||||
<SuggestedChatMessages slug={workspace.slug} />
|
||||
<WorkspacePfp workspace={workspace} slug={slug} />
|
||||
<DeleteWorkspace workspace={workspace} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -76,7 +76,7 @@ function ShowWorkspaceChat() {
|
||||
|
||||
const TabContent = TABS[tab];
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||
<div className="w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
|
||||
@ -213,8 +213,18 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
|
||||
});
|
||||
}
|
||||
|
||||
let _agentSessionActive = false;
|
||||
export function setAgentSessionActive(value) {
|
||||
_agentSessionActive = value;
|
||||
}
|
||||
export function getAgentSessionActive() {
|
||||
return _agentSessionActive;
|
||||
}
|
||||
|
||||
export function useIsAgentSessionActive() {
|
||||
const [activeSession, setActiveSession] = useState(false);
|
||||
const [activeSession, setActiveSession] = useState(
|
||||
() => !!getAgentSessionActive()
|
||||
);
|
||||
useEffect(() => {
|
||||
function listenForAgentSession() {
|
||||
if (!window) return;
|
||||
|
||||
@ -158,10 +158,7 @@ export default function handleChat(
|
||||
}
|
||||
|
||||
// Action Handling via special 'action' attribute on response.
|
||||
if (action === "reset_chat") {
|
||||
// Chat was reset, keep reset message and clear everything else.
|
||||
setChatHistory([_chatHistory.pop()]);
|
||||
}
|
||||
if (action === "reset_chat") setChatHistory([]);
|
||||
|
||||
// If thread was updated automatically based on chat prompt
|
||||
// then we can handle the updating of the thread here.
|
||||
|
||||
@ -216,9 +216,9 @@ function workspaceThreadEndpoints(app) {
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { chatId, newText = null } = reqBody(request);
|
||||
const { chatId, newText = null, role = "assistant" } = reqBody(request);
|
||||
if (!newText || !String(newText).trim())
|
||||
throw new Error("Cannot save empty response");
|
||||
throw new Error("Cannot save empty edit");
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
@ -231,15 +231,20 @@ function workspaceThreadEndpoints(app) {
|
||||
});
|
||||
if (!existingChat) throw new Error("Invalid chat.");
|
||||
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
if (role === "user") {
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
prompt: String(newText),
|
||||
});
|
||||
} else {
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
|
||||
@ -454,9 +454,9 @@ function workspaceEndpoints(app) {
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { chatId, newText = null } = reqBody(request);
|
||||
const { chatId, newText = null, role = "assistant" } = reqBody(request);
|
||||
if (!newText || !String(newText).trim())
|
||||
throw new Error("Cannot save empty response");
|
||||
throw new Error("Cannot save empty edit");
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
const workspace = response.locals.workspace;
|
||||
@ -468,15 +468,20 @@ function workspaceEndpoints(app) {
|
||||
});
|
||||
if (!existingChat) throw new Error("Invalid chat.");
|
||||
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
if (role === "user") {
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
prompt: String(newText),
|
||||
});
|
||||
} else {
|
||||
const chatResponse = safeJsonParse(existingChat.response, null);
|
||||
if (!chatResponse) throw new Error("Failed to parse chat response");
|
||||
await WorkspaceChats._update(existingChat.id, {
|
||||
response: JSON.stringify({
|
||||
...chatResponse,
|
||||
text: String(newText),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
|
||||
@ -19,7 +19,7 @@ async function resetMemory(
|
||||
return {
|
||||
uuid: msgUUID,
|
||||
type: "textResponse",
|
||||
textResponse: "Workspace chat memory was reset!",
|
||||
textResponse: "Chat memory was reset!",
|
||||
sources: [],
|
||||
close: true,
|
||||
error: false,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user