Frontend audit: Bug fixes and refinements (#2112)

* (fix:attachements) sep id for redux ops

* (fix:ui) popups, toast, share modal

* (feat:agentsPreview) stable preview, ui fixes

* (fix:ui) light theme icon, sleek scroll

* (chore:i18n) missin keys

* (chore:i18n) missing keys

* (feat:preferrenceSlice) autoclear invalid source from storage

* (fix:general) delete all conv close btn

* (fix:tts) play one at a time

* (fix:tts) gracefully unmount

* (feat:tts) audio LRU cache

* (feat:tts) pointer on hovered area

* (feat:tts) clean text for speach

---------

Co-authored-by: GH Action - Upstream Sync <action@github.com>
This commit is contained in:
Manish Madan
2025-10-29 05:17:26 +05:30
committed by GitHub
parent 94f70e6de5
commit 6a4cb617f9
40 changed files with 1805 additions and 490 deletions

View File

@@ -130,11 +130,15 @@ class TextToSpeech(Resource):
@api.expect(tts_model)
@api.doc(description="Synthesize audio speech from text")
def post(self):
from application.utils import clean_text_for_tts
data = request.get_json()
text = data["text"]
cleaned_text = clean_text_for_tts(text)
try:
tts_instance = TTSCreator.create_tts(settings.TTS_PROVIDER)
audio_base64, detected_language = tts_instance.text_to_speech(text)
audio_base64, detected_language = tts_instance.text_to_speech(cleaned_text)
return make_response(
jsonify(
{

View File

@@ -187,3 +187,44 @@ def generate_image_url(image_path):
else:
base_url = getattr(settings, "API_URL", "http://localhost:7091")
return f"{base_url}/api/images/{image_path}"
def clean_text_for_tts(text: str) -> str:
"""
clean text for Text-to-Speech processing.
"""
# Handle code blocks and links
text = re.sub(r'```mermaid[\s\S]*?```', ' flowchart, ', text) ## ```mermaid...```
text = re.sub(r'```[\s\S]*?```', ' code block, ', text) ## ```code```
text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text) ## [text](url)
text = re.sub(r'!\[([^\]]*)\]\([^\)]+\)', '', text) ## ![alt](url)
# Remove markdown formatting
text = re.sub(r'`([^`]+)`', r'\1', text) ## `code`
text = re.sub(r'\{([^}]*)\}', r' \1 ', text) ## {text}
text = re.sub(r'[{}]', ' ', text) ## unmatched {}
text = re.sub(r'\[([^\]]+)\]', r' \1 ', text) ## [text]
text = re.sub(r'[\[\]]', ' ', text) ## unmatched []
text = re.sub(r'(\*\*|__)(.*?)\1', r'\2', text) ## **bold** __bold__
text = re.sub(r'(\*|_)(.*?)\1', r'\2', text) ## *italic* _italic_
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) ## # headers
text = re.sub(r'^>\s+', '', text, flags=re.MULTILINE) ## > blockquotes
text = re.sub(r'^[\s]*[-\*\+]\s+', '', text, flags=re.MULTILINE) ## - * + lists
text = re.sub(r'^[\s]*\d+\.\s+', '', text, flags=re.MULTILINE) ## 1. numbered lists
text = re.sub(r'^[\*\-_]{3,}\s*$', '', text, flags=re.MULTILINE) ## --- *** ___ rules
text = re.sub(r'<[^>]*>', '', text) ## <html> tags
#Remove non-ASCII (emojis, special Unicode)
text = re.sub(r'[^\x20-\x7E\n\r\t]', '', text)
#Replace special sequences
text = re.sub(r'-->', ', ', text) ## -->
text = re.sub(r'<--', ', ', text) ## <--
text = re.sub(r'=>', ', ', text) ## =>
text = re.sub(r'::', ' ', text) ## ::
#Normalize whitespace
text = re.sub(r'\s+', ' ', text)
text = text.strip()
return text

View File

@@ -411,7 +411,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
{recentAgents?.length > 0 ? (
<div>
<div className="mx-4 my-auto mt-2 flex h-6 items-center">
<p className="mt-1 ml-4 text-sm font-semibold">Agents</p>
<p className="mt-1 ml-4 text-sm font-semibold">
{t('navigation.agents')}
</p>
</div>
<div className="agents-container">
<div>

View File

@@ -1,13 +1,16 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
export default function PageNotFound() {
const { t } = useTranslation();
return (
<div className="dark:bg-raisin-black grid min-h-screen">
<p className="text-jet dark:bg-outer-space mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 lg:p-10 xl:p-16 dark:text-gray-100">
<h1>404</h1>
<p>The page you are looking for does not exist.</p>
<h1>{t('pageNotFound.title')}</h1>
<p>{t('pageNotFound.message')}</p>
<button className="pointer-cursor bg-blue-1000 hover:bg-blue-3000 mr-4 flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-white transition-colors duration-100">
<Link to="/">Go Back Home</Link>
<Link to="/">{t('pageNotFound.goHome')}</Link>
</button>
</p>
</div>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
@@ -11,6 +12,7 @@ import Logs from '../settings/Logs';
import { Agent } from './types';
export default function AgentLogs() {
const { t } = useTranslation();
const navigate = useNavigate();
const { agentId } = useParams();
const token = useSelector(selectToken);
@@ -45,12 +47,12 @@ export default function AgentLogs() {
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
Back to all agents
{t('agents.backToAll')}
</p>
</div>
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
<h1 className="text-eerie-black m-0 text-[32px] font-bold md:text-[40px] dark:text-white">
Agent Logs
{t('agents.logs.title')}
</h1>
</div>
<div className="mt-6 flex flex-col gap-3 px-4">
@@ -59,9 +61,10 @@ export default function AgentLogs() {
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
<p className="text-xs text-[#28292E] dark:text-[#E0E0E0]/40">
{agent.last_used_at
? 'Last used at ' +
? t('agents.logs.lastUsedAt') +
' ' +
new Date(agent.last_used_at).toLocaleString()
: 'No usage history'}
: t('agents.logs.noUsageHistory')}
</p>
</div>
)}
@@ -79,7 +82,9 @@ export default function AgentLogs() {
<Spinner />
</div>
) : (
agent && <Logs agentId={agent.id} tableHeader="Agent endpoint logs" />
agent && (
<Logs agentId={agent.id} tableHeader={t('agents.logs.tableHeader')} />
)
)}
</div>
);

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import MessageInput from '../components/MessageInput';
@@ -17,6 +18,7 @@ import { selectSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
export default function AgentPreview() {
const { t } = useTranslation();
const dispatch = useDispatch<AppDispatch>();
const queries = useSelector(selectPreviewQueries);
@@ -130,8 +132,7 @@ export default function AgentPreview() {
/>
</div>
<p className="text-gray-4000 dark:text-sonic-silver w-full bg-transparent text-center text-xs md:inline">
This is a preview of the agent. You can publish it to start using it
in conversations.
{t('agents.preview.testMessage')}
</p>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
@@ -17,6 +18,7 @@ import { agentSectionsConfig } from './agents.config';
import { Agent } from './types';
export default function AgentsList() {
const { t } = useTranslation();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const selectedAgent = useSelector(selectSelectedAgent);
@@ -33,11 +35,10 @@ export default function AgentsList() {
return (
<div className="p-4 md:p-12">
<h1 className="text-eerie-black mb-0 text-[32px] font-bold lg:text-[40px] dark:text-[#E0E0E0]">
Agents
{t('agents.title')}
</h1>
<p className="dark:text-gray-4000 mt-5 text-[15px] text-[#71717A]">
Discover and create custom versions of DocsGPT that combine
instructions, extra knowledge, and any combination of skills
{t('agents.description')}
</p>
{agentSectionsConfig.map((sectionConfig) => (
<AgentSection key={sectionConfig.id} config={sectionConfig} />
@@ -51,6 +52,7 @@ function AgentSection({
}: {
config: (typeof agentSectionsConfig)[number];
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
@@ -85,16 +87,18 @@ function AgentSection({
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-2">
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
{config.title}
{t(`agents.sections.${config.id}.title`)}
</h2>
<p className="text-[13px] text-[#71717A]">{config.description}</p>
<p className="text-[13px] text-[#71717A]">
{t(`agents.sections.${config.id}.description`)}
</p>
</div>
{config.showNewAgentButton && (
<button
className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-4 py-2 text-sm text-white"
onClick={() => navigate('/agents/new')}
>
New Agent
{t('agents.newAgent')}
</button>
)}
</div>
@@ -117,13 +121,13 @@ function AgentSection({
</div>
) : (
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
<p>{config.emptyStateDescription}</p>
<p>{t(`agents.sections.${config.id}.emptyState`)}</p>
{config.showNewAgentButton && (
<button
className="bg-purple-30 hover:bg-violets-are-blue ml-2 rounded-full px-4 py-2 text-sm text-white"
onClick={() => navigate('/agents/new')}
>
New Agent
{t('agents.newAgent')}
</button>
)}
</div>

View File

@@ -1,5 +1,6 @@
import isEqual from 'lodash/isEqual';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
@@ -30,6 +31,7 @@ const embeddingsName =
'huggingface_sentence-transformers/all-mpnet-base-v2';
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const { agentId } = useParams();
@@ -87,8 +89,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const modeConfig = {
new: {
heading: 'New Agent',
buttonText: 'Publish',
heading: t('agents.form.headings.new'),
buttonText: t('agents.form.buttons.publish'),
showDelete: false,
showSaveDraft: true,
showLogs: false,
@@ -96,8 +98,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
trackChanges: false,
},
edit: {
heading: 'Edit Agent',
buttonText: 'Save',
heading: t('agents.form.headings.edit'),
buttonText: t('agents.form.buttons.save'),
showDelete: true,
showSaveDraft: false,
showLogs: true,
@@ -105,8 +107,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
trackChanges: true,
},
draft: {
heading: 'New Agent (Draft)',
buttonText: 'Publish',
heading: t('agents.form.headings.draft'),
buttonText: t('agents.form.buttons.publish'),
showDelete: true,
showSaveDraft: true,
showLogs: false,
@@ -116,8 +118,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
};
const chunks = ['0', '2', '4', '6', '8', '10'];
const agentTypes = [
{ label: 'Classic', value: 'classic' },
{ label: 'ReAct', value: 'react' },
{ label: t('agents.form.agentTypes.classic'), value: 'classic' },
{ label: t('agents.form.agentTypes.react'), value: 'react' },
];
const isPublishable = () => {
@@ -543,7 +545,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
Back to all agents
{t('agents.backToAll')}
</p>
</div>
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
@@ -555,7 +557,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
className="text-purple-30 dark:text-light-gray mr-4 rounded-3xl py-2 text-sm font-medium dark:bg-transparent"
onClick={handleCancel}
>
Cancel
{t('agents.form.buttons.cancel')}
</button>
{modeConfig[effectiveMode].showDelete && agent.id && (
<button
@@ -563,7 +565,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
onClick={() => setDeleteConfirmation('ACTIVE')}
>
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
Delete
{t('agents.form.buttons.delete')}
</button>
)}
{modeConfig[effectiveMode].showSaveDraft && (
@@ -578,7 +580,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
{draftLoading ? (
<Spinner size="small" color="#976af3" />
) : (
'Save Draft'
t('agents.form.buttons.saveDraft')
)}
</span>
</button>
@@ -589,7 +591,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
onClick={() => navigate(`/agents/logs/${agent.id}`)}
>
<span className="block h-5 w-5 bg-[url('/src/assets/monitoring-purple.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/monitoring-white.svg')]" />
Logs
{t('agents.form.buttons.logs')}
</button>
)}
{modeConfig[effectiveMode].showAccessDetails && (
@@ -597,7 +599,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
className="hover:bg-vi</button>olets-are-blue border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
onClick={() => setAgentDetails('ACTIVE')}
>
Access Details
{t('agents.form.buttons.accessDetails')}
</button>
)}
<button
@@ -618,17 +620,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<div className="mt-3 flex w-full flex-1 grid-cols-5 flex-col gap-10 rounded-[30px] bg-[#F6F6F6] p-5 max-[1179px]:overflow-visible min-[1180px]:grid min-[1180px]:gap-5 min-[1180px]:overflow-hidden dark:bg-[#383838]">
<div className="scrollbar-thin col-span-2 flex flex-col gap-5 max-[1179px]:overflow-visible min-[1180px]:max-h-full min-[1180px]:overflow-y-auto min-[1180px]:pr-3">
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Meta</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.meta')}
</h2>
<input
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
type="text"
value={agent.name}
placeholder="Agent name"
placeholder={t('agents.form.placeholders.agentName')}
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
/>
<textarea
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 h-32 w-full rounded-xl border bg-white px-5 py-4 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
placeholder="Describe your agent"
placeholder={t('agents.form.placeholders.describeAgent')}
value={agent.description}
onChange={(e) =>
setAgent({ ...agent, description: e.target.value })
@@ -641,9 +645,12 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
onUpload={handleUpload}
onRemove={() => setImageFile(null)}
uploadText={[
{ text: 'Click to upload', colorClass: 'text-[#7D54D1]' },
{
text: ' or drag and drop',
text: t('agents.form.upload.clickToUpload'),
colorClass: 'text-[#7D54D1]',
},
{
text: t('agents.form.upload.dragAndDrop'),
colorClass: 'text-[#525252]',
},
]}
@@ -651,7 +658,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</div>
</div>
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Source</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.source')}
</h2>
<div className="mt-3">
<div className="flex flex-wrap items-center gap-1">
<button
@@ -672,11 +681,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
source.name === id ||
source.retriever === id,
);
return matchedDoc?.name || `External KB`;
return (
matchedDoc?.name || t('agents.form.externalKb')
);
})
.filter(Boolean)
.join(', ')
: 'Select sources'}
: t('agents.form.placeholders.selectSources')}
</button>
<MultiSelectPopup
isOpen={isSourcePopupOpen}
@@ -720,9 +731,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
setSelectedSourceIds(newSelectedIds);
}
}}
title="Select Sources"
searchPlaceholder="Search sources..."
noOptionsMessage="No sources available"
title={t('agents.form.sourcePopup.title')}
searchPlaceholder={t(
'agents.form.sourcePopup.searchPlaceholder',
)}
noOptionsMessage={t(
'agents.form.sourcePopup.noOptionsMessage',
)}
/>
</div>
<div className="mt-3">
@@ -737,7 +752,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
border="border"
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
placeholder="Chunks per query"
placeholder={t('agents.form.placeholders.chunksPerQuery')}
placeholderClassName="text-gray-400 dark:text-silver"
contentSize="text-sm"
/>
@@ -757,7 +772,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
setAgent({ ...agent, prompt_id: id })
}
setPrompts={setPrompts}
title="Prompt"
title={t('agents.form.sections.prompt')}
titleClassName="text-lg font-semibold dark:text-[#E0E0E0]"
showAddButton={false}
dropdownProps={{
@@ -777,12 +792,14 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue w-20 shrink-0 basis-full rounded-3xl border-2 border-solid px-5 py-[11px] text-sm transition-colors hover:text-white sm:basis-auto"
onClick={() => setAddPromptModal('ACTIVE')}
>
Add
{t('agents.form.buttons.add')}
</button>
</div>
</div>
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Tools</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.tools')}
</h2>
<div className="mt-3 flex flex-wrap items-center gap-1">
<button
ref={toolAnchorButtonRef}
@@ -798,7 +815,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
.map((tool) => tool.display_name || tool.name)
.filter(Boolean)
.join(', ')
: 'Select tools'}
: t('agents.form.placeholders.selectTools')}
</button>
<MultiSelectPopup
isOpen={isToolsPopupOpen}
@@ -817,14 +834,18 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
})),
)
}
title="Select Tools"
searchPlaceholder="Search tools..."
noOptionsMessage="No tools available"
title={t('agents.form.toolsPopup.title')}
searchPlaceholder={t(
'agents.form.toolsPopup.searchPlaceholder',
)}
noOptionsMessage={t('agents.form.toolsPopup.noOptionsMessage')}
/>
</div>
</div>
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Agent type</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.agentType')}
</h2>
<div className="mt-3">
<Dropdown
options={agentTypes}
@@ -842,7 +863,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
border="border"
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
placeholder="Select type"
placeholder={t('agents.form.placeholders.selectType')}
placeholderClassName="text-gray-400 dark:text-silver"
contentSize="text-sm"
/>
@@ -856,7 +877,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
className="flex w-full items-center justify-between text-left focus:outline-none"
>
<div>
<h2 className="text-lg font-semibold">Advanced</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.advanced')}
</h2>
</div>
<div className="ml-4 flex items-center">
<svg
@@ -879,9 +902,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
{isAdvancedSectionExpanded && (
<div className="mt-3">
<div>
<h2 className="text-sm font-medium">JSON response schema</h2>
<h2 className="text-sm font-medium">
{t('agents.form.advanced.jsonSchema')}
</h2>
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
Define a JSON schema to enforce structured output format
{t('agents.form.advanced.jsonSchemaDescription')}
</p>
</div>
<textarea
@@ -915,17 +940,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
}`}
/>
{jsonSchemaValid
? 'Valid JSON'
: 'Invalid JSON - fix to enable saving'}
? t('agents.form.advanced.validJson')
: t('agents.form.advanced.invalidJson')}
</div>
)}
<div className="mt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Token limiting</h2>
<h2 className="text-sm font-medium">
{t('agents.form.advanced.tokenLimiting')}
</h2>
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
Limit daily total tokens that can be used by this agent
{t('agents.form.advanced.tokenLimitingDescription')}
</p>
</div>
<button
@@ -965,7 +992,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
})
}
disabled={!agent.limited_token_mode}
placeholder="Enter token limit"
placeholder={t('agents.form.placeholders.enterTokenLimit')}
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
!agent.limited_token_mode
? 'cursor-not-allowed opacity-50'
@@ -977,10 +1004,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
<div className="mt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">Request limiting</h2>
<h2 className="text-sm font-medium">
{t('agents.form.advanced.requestLimiting')}
</h2>
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
Limit daily total requests that can be made to this
agent
{t('agents.form.advanced.requestLimitingDescription')}
</p>
</div>
<button
@@ -1020,7 +1048,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
})
}
disabled={!agent.limited_request_mode}
placeholder="Enter request limit"
placeholder={t(
'agents.form.placeholders.enterRequestLimit',
)}
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
!agent.limited_request_mode
? 'cursor-not-allowed opacity-50'
@@ -1033,22 +1063,24 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
</div>
</div>
<div className="col-span-3 flex flex-col gap-2 max-[1179px]:h-auto max-[1179px]:px-0 max-[1179px]:py-0 min-[1180px]:h-full min-[1180px]:py-2 dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Preview</h2>
<h2 className="text-lg font-semibold">
{t('agents.form.sections.preview')}
</h2>
<div className="flex-1 max-[1179px]:overflow-visible min-[1180px]:min-h-0 min-[1180px]:overflow-hidden">
<AgentPreviewArea />
</div>
</div>
</div>
<ConfirmationModal
message="Are you sure you want to delete this agent?"
message={t('agents.deleteConfirmation')}
modalState={deleteConfirmation}
setModalState={setDeleteConfirmation}
submitLabel="Delete"
submitLabel={t('agents.form.buttons.delete')}
handleSubmit={() => {
handleDelete(agent.id || '');
setDeleteConfirmation('INACTIVE');
}}
cancelLabel="Cancel"
cancelLabel={t('agents.form.buttons.cancel')}
variant="danger"
/>
<AgentDetailsModal
@@ -1071,6 +1103,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
}
function AgentPreviewArea() {
const { t } = useTranslation();
const selectedAgent = useSelector(selectSelectedAgent);
return (
<div className="dark:bg-raisin-black w-full rounded-[30px] border border-[#F6F6F6] bg-white max-[1179px]:h-[600px] min-[1180px]:h-full dark:border-[#7E7E7E]">
@@ -1082,7 +1115,7 @@ function AgentPreviewArea() {
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
<p className="dark:text-gray-4000 text-xs text-[#18181B]">
Published agents can be previewed here
{t('agents.form.preview.publishedPreview')}
</p>
</div>
)}

View File

@@ -144,7 +144,7 @@ export default function SharedAgent() {
className="mx-auto mb-6 h-32 w-32"
/>
<p className="dark:text-gray-4000 text-center text-lg text-[#71717A]">
No agent found. Please ensure the agent is shared.
{t('agents.shared.notFound')}
</p>
</div>
</div>

View File

@@ -45,7 +45,7 @@ export default function ActionButtons({
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
{showNewChat && (
<button
title="Open New Chat"
title={t('actionButtons.openNewChat')}
onClick={newChat}
className="hover:bg-bright-gray flex items-center gap-1 rounded-full p-2 lg:hidden dark:hover:bg-[#28292E]"
>
@@ -62,7 +62,7 @@ export default function ActionButtons({
{showShare && conversationId && (
<>
<button
title="Share"
title={t('actionButtons.share')}
onClick={() => setShareModalState(true)}
className="hover:bg-bright-gray rounded-full p-2 dark:hover:bg-[#28292E]"
>

View File

@@ -38,7 +38,7 @@ interface DirectoryStructure {
[key: string]: FileNode;
}
interface ConnectorTreeComponentProps {
interface ConnectorTreeProps {
docId: string;
sourceName: string;
onBackToDocuments: () => void;
@@ -50,7 +50,7 @@ interface SearchResult {
isFile: boolean;
}
const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
const ConnectorTree: React.FC<ConnectorTreeProps> = ({
docId,
sourceName,
onBackToDocuments,
@@ -744,4 +744,4 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
);
};
export default ConnectorTreeComponent;
export default ConnectorTree;

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { formatBytes } from '../utils/stringUtils';
import { formatDate } from '../utils/dateTimeUtils';
import {
@@ -66,6 +67,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
);
};
const { t } = useTranslation();
const [files, setFiles] = useState<CloudFile[]>([]);
const [selectedFiles, setSelectedFiles] =
useState<string[]>(initialSelectedFiles);
@@ -417,7 +419,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
<div className="mb-3 max-w-md">
<Input
type="text"
placeholder="Search files and folders..."
placeholder={t('filePicker.searchPlaceholder')}
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
colorVariant="silver"
@@ -431,7 +433,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
{/* Selected Files Message */}
<div className="pb-3 text-sm text-gray-600 dark:text-gray-400">
{selectedFiles.length + selectedFolders.length} selected
{t('filePicker.itemsSelected', {
count: selectedFiles.length + selectedFolders.length,
})}
</div>
</div>
@@ -448,9 +452,15 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
<TableHead>
<TableRow>
<TableHeader width="40px"></TableHeader>
<TableHeader width="60%">Name</TableHeader>
<TableHeader width="20%">Last Modified</TableHeader>
<TableHeader width="20%">Size</TableHeader>
<TableHeader width="60%">
{t('filePicker.name')}
</TableHeader>
<TableHeader width="20%">
{t('filePicker.lastModified')}
</TableHeader>
<TableHeader width="20%">
{t('filePicker.size')}
</TableHeader>
</TableRow>
</TableHead>
<TableBody>

View File

@@ -36,7 +36,7 @@ interface DirectoryStructure {
[key: string]: FileNode;
}
interface FileTreeComponentProps {
interface FileTreeProps {
docId: string;
sourceName: string;
onBackToDocuments: () => void;
@@ -48,7 +48,7 @@ interface SearchResult {
isFile: boolean;
}
const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
const FileTree: React.FC<FileTreeProps> = ({
docId,
sourceName,
onBackToDocuments,
@@ -871,4 +871,4 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
);
};
export default FileTreeComponent;
export default FileTree;

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDropzone } from 'react-dropzone';
import { twMerge } from 'tailwind-merge';
@@ -44,13 +45,14 @@ export const FileUpload = ({
activeClassName = 'border-blue-500 bg-blue-50',
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
uploadText = 'Click to upload or drag and drop',
dragActiveText = 'Drop the files here',
fileTypeText = 'PNG, JPG, JPEG up to',
sizeLimitText = 'MB',
uploadText,
dragActiveText,
fileTypeText,
sizeLimitText,
disabled = false,
validator,
}: FileUploadProps) => {
const { t } = useTranslation();
const [errors, setErrors] = useState<string[]>([]);
const [preview, setPreview] = useState<string | null>(null);
const [currentFile, setCurrentFile] = useState<File | null>(null);
@@ -71,7 +73,9 @@ export const FileUpload = ({
if (file.size > maxSize) {
return {
isValid: false,
error: `File exceeds ${maxSize / 1024 / 1024}MB limit`,
error: t('components.fileUpload.fileSizeError', {
size: maxSize / 1024 / 1024,
}),
};
}
@@ -178,7 +182,11 @@ export const FileUpload = ({
</p>
);
}
return <p className="text-sm font-semibold">{uploadText}</p>;
return (
<p className="text-sm font-semibold">
{uploadText || t('components.fileUpload.clickToUpload')}
</p>
);
};
const defaultContent = (
@@ -196,14 +204,17 @@ export const FileUpload = ({
<div className="text-center">
<div className="text-sm font-medium">
{isDragActive ? (
<p className="text-sm font-semibold">{dragActiveText}</p>
<p className="text-sm font-semibold">
{dragActiveText || t('components.fileUpload.dropFiles')}
</p>
) : (
renderUploadText()
)}
</div>
<p className="mt-1 text-xs text-[#A3A3A3]">
{fileTypeText} {maxSize / 1024 / 1024}
{sizeLimitText}
{fileTypeText || t('components.fileUpload.fileTypes')}{' '}
{maxSize / 1024 / 1024}
{sizeLimitText || t('components.fileUpload.sizeLimitUnit')}
</p>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import mermaid from 'mermaid';
import CopyButton from './CopyButton';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
@@ -15,6 +16,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
code,
isLoading,
}) => {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const diagramId = useRef(
`mermaid-${Date.now()}-${Math.random().toString(36).substring(2)}`,
@@ -273,7 +275,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
<button
onClick={() => setShowDownloadMenu(!showDownloadMenu)}
className="flex h-full items-center rounded-sm bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
title="Download options"
title={t('mermaid.downloadOptions')}
>
Download <span className="ml-1"></span>
</button>
@@ -307,7 +309,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
? 'bg-blue-200 dark:bg-blue-800'
: 'bg-gray-100 dark:bg-gray-700'
}`}
title="View Code"
title={t('mermaid.viewCode')}
>
Code
</button>
@@ -353,7 +355,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
setZoomFactor((prev) => Math.max(1, prev - 0.5))
}
className="rounded px-1 hover:bg-gray-600"
title="Decrease zoom"
title={t('mermaid.decreaseZoom')}
>
-
</button>
@@ -362,7 +364,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
onClick={() => {
setZoomFactor(2);
}}
title="Reset zoom"
title={t('mermaid.resetZoom')}
>
{zoomFactor.toFixed(1)}x
</span>
@@ -371,7 +373,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
setZoomFactor((prev) => Math.min(6, prev + 0.5))
}
className="rounded px-1 hover:bg-gray-600"
title="Increase zoom"
title={t('mermaid.increaseZoom')}
>
+
</button>

View File

@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import close from '../assets/cross.svg';
import rightArrow from '../assets/arrow-full-right.svg';
import bg from '../assets/notification-bg.jpg';
@@ -13,13 +14,14 @@ export default function Notification({
notificationLink,
handleCloseNotification,
}: NotificationProps) {
const { t } = useTranslation();
return (
<a
className="absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 rounded-lg bg-cover bg-center bg-no-repeat px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
style={{ backgroundImage: `url(${bg})` }}
href={notificationLink}
target="_blank"
aria-label="Notification"
aria-label={t('notification.ariaLabel')}
rel="noreferrer"
>
<p className="text-white-3000 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
@@ -31,7 +33,7 @@ export default function Notification({
<button
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
aria-label="Close notification"
aria-label={t('notification.closeAriaLabel')}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();

View File

@@ -24,6 +24,7 @@ interface SettingsBarProps {
}
const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
const { t } = useTranslation();
const [hiddenGradient, setHiddenGradient] =
useState<HiddenGradientType>('left');
const containerRef = useRef<null | HTMLDivElement>(null);
@@ -60,7 +61,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<button
onClick={() => scrollTabs(-1)}
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs left"
aria-label={t('settings.scrollTabsLeft')}
>
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
</button>
@@ -69,7 +70,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
ref={containerRef}
className="no-scrollbar flex snap-x flex-nowrap overflow-x-auto scroll-smooth md:space-x-4"
role="tablist"
aria-label="Settings tabs"
aria-label={t('settings.tabsAriaLabel')}
>
{tabs.map((tab, index) => (
<button
@@ -93,7 +94,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<button
onClick={() => scrollTabs(1)}
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs right"
aria-label={t('settings.scrollTabsRight')}
>
<img src={ArrowRight} alt="right-arrow" className="h-3" />
</button>

View File

@@ -172,11 +172,7 @@ export default function SourcesPopup({
: doc.date !== option.date,
)
: [];
dispatch(
setSelectedDocs(
updatedDocs.length > 0 ? updatedDocs : null,
),
);
dispatch(setSelectedDocs(updatedDocs));
handlePostDocumentSelect(
updatedDocs.length > 0 ? updatedDocs : null,
);

View File

@@ -1,94 +1,202 @@
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import Speaker from '../assets/speaker.svg?react';
import Stopspeech from '../assets/stopspeech.svg?react';
import LoadingIcon from '../assets/Loading.svg?react'; // Add a loading icon SVG here
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export default function SpeakButton({
text,
colorLight,
colorDark,
}: {
text: string;
colorLight?: string;
colorDark?: string;
}) {
let currentlyPlayingAudio: {
audio: HTMLAudioElement;
stopCallback: () => void;
} | null = null;
let currentLoadingRequest: {
abortController: AbortController;
stopLoadingCallback: () => void;
} | null = null;
// LRU Cache for audio
const audioCache = new Map<string, string>();
const MAX_CACHE_SIZE = 10;
function getCachedAudio(text: string): string | undefined {
const cached = audioCache.get(text);
if (cached) {
audioCache.delete(text);
audioCache.set(text, cached);
}
return cached;
}
function setCachedAudio(text: string, audioBase64: string) {
if (audioCache.has(text)) {
audioCache.delete(text);
}
if (audioCache.size >= MAX_CACHE_SIZE) {
const firstKey = audioCache.keys().next().value;
if (firstKey !== undefined) {
audioCache.delete(firstKey);
}
}
audioCache.set(text, audioBase64);
}
export default function SpeakButton({ text }: { text: string }) {
const [isSpeaking, setIsSpeaking] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSpeakHovered, setIsSpeakHovered] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
useEffect(() => {
return () => {
// Abort any pending fetch request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
// Stop any playing audio
if (audioRef.current) {
audioRef.current.pause();
if (currentlyPlayingAudio?.audio === audioRef.current) {
currentlyPlayingAudio = null;
}
audioRef.current = null;
}
// Clear global loading request if it's this component's
if (currentLoadingRequest) {
currentLoadingRequest = null;
}
};
}, []);
const handleSpeakClick = async () => {
if (isSpeaking) {
// Stop audio if it's currently playing
audioRef.current?.pause();
audioRef.current = null;
currentlyPlayingAudio = null;
setIsSpeaking(false);
return;
}
// Stop any currently playing audio
if (currentlyPlayingAudio) {
currentlyPlayingAudio.audio.pause();
currentlyPlayingAudio.stopCallback();
currentlyPlayingAudio = null;
}
// Abort any pending loading request
if (currentLoadingRequest) {
currentLoadingRequest.abortController.abort();
currentLoadingRequest.stopLoadingCallback();
currentLoadingRequest = null;
}
try {
// Set loading state and initiate TTS request
setIsLoading(true);
const cachedAudio = getCachedAudio(text);
let audioBase64: string;
const response = await fetch(apiHost + '/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const data = await response.json();
if (data.success && data.audio_base64) {
// Create and play the audio
const audio = new Audio(`data:audio/mp3;base64,${data.audio_base64}`);
audioRef.current = audio;
audio.play().then(() => {
setIsSpeaking(true);
setIsLoading(false);
// Reset when audio ends
audio.onended = () => {
setIsSpeaking(false);
audioRef.current = null;
};
});
} else {
console.error('Failed to retrieve audio.');
if (cachedAudio) {
audioBase64 = cachedAudio;
setIsLoading(false);
} else {
const abortController = new AbortController();
abortControllerRef.current = abortController;
currentLoadingRequest = {
abortController,
stopLoadingCallback: () => {
setIsLoading(false);
},
};
const response = await fetch(apiHost + '/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
signal: abortController.signal,
});
const data = await response.json();
abortControllerRef.current = null;
currentLoadingRequest = null;
if (data.success && data.audio_base64) {
audioBase64 = data.audio_base64;
// Store in cache
setCachedAudio(text, audioBase64);
setIsLoading(false);
} else {
console.error('Failed to retrieve audio.');
setIsLoading(false);
return;
}
}
const audio = new Audio(`data:audio/mp3;base64,${audioBase64}`);
audioRef.current = audio;
currentlyPlayingAudio = {
audio,
stopCallback: () => {
setIsSpeaking(false);
audioRef.current = null;
},
};
audio.play().then(() => {
setIsSpeaking(true);
setIsLoading(false);
audio.onended = () => {
setIsSpeaking(false);
audioRef.current = null;
if (currentlyPlayingAudio?.audio === audio) {
currentlyPlayingAudio = null;
}
};
});
} catch (error: any) {
abortControllerRef.current = null;
currentLoadingRequest = null;
if (error.name === 'AbortError') {
return;
}
} catch (error) {
console.error('Error fetching audio from TTS endpoint', error);
setIsLoading(false);
}
};
return (
<div
className={`flex items-center justify-center rounded-full p-2 ${
isSpeakHovered
? `dark:bg-purple-taupe bg-[#EEEEEE]`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
<button
type="button"
className={`flex cursor-pointer items-center justify-center rounded-full p-2 ${
isSpeaking || isLoading
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
: 'bg-white-3000 dark:hover:bg-purple-taupe hover:bg-[#EEEEEE] dark:bg-transparent'
}`}
onClick={handleSpeakClick}
aria-label={
isLoading
? 'Loading audio'
: isSpeaking
? 'Stop speaking'
: 'Speak text'
}
disabled={isLoading}
>
{isLoading ? (
<LoadingIcon className="animate-spin" />
) : isSpeaking ? (
<Stopspeech
className="cursor-pointer fill-none"
onClick={handleSpeakClick}
onMouseEnter={() => setIsSpeakHovered(true)}
onMouseLeave={() => setIsSpeakHovered(false)}
/>
<Stopspeech className="fill-none" />
) : (
<Speaker
className="cursor-pointer fill-none"
onClick={handleSpeakClick}
onMouseEnter={() => setIsSpeakHovered(true)}
onMouseLeave={() => setIsSpeakHovered(false)}
/>
<Speaker className="fill-none" />
)}
</div>
</button>
);
}

View File

@@ -560,37 +560,47 @@ const ConversationBubble = forwardRef<
{handleFeedback && (
<>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
<Like
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'LIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('LIKE');
}
}}
></Like>
</div>
</div>
<button
type="button"
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
onClick={() => {
if (feedback === 'LIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('LIKE');
}
}}
aria-label={
feedback === 'LIKE' ? 'Remove like' : 'Like'
}
>
<Like
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
></Like>
</button>
</div>
<div className="relative mr-2 flex items-center justify-center">
<div>
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
<Dislike
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
onClick={() => {
if (feedback === 'DISLIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('DISLIKE');
}
}}
></Dislike>
</div>
</div>
<button
type="button"
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
onClick={() => {
if (feedback === 'DISLIKE') {
handleFeedback?.(null);
} else {
handleFeedback?.('DISLIKE');
}
}}
aria-label={
feedback === 'DISLIKE'
? 'Remove dislike'
: 'Dislike'
}
>
<Dislike
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
></Dislike>
</button>
</div>
</>
)}
@@ -793,6 +803,7 @@ function Thought({
thought: string;
preprocessLaTeX: (content: string) => string;
}) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const [isThoughtOpen, setIsThoughtOpen] = useState(true);
@@ -813,7 +824,9 @@ function Thought({
className="flex flex-row items-center gap-2"
onClick={() => setIsThoughtOpen(!isThoughtOpen)}
>
<p className="text-base font-semibold">Reasoning</p>
<p className="text-base font-semibold">
{t('conversation.reasoning')}
</p>
<img
src={ChevronDown}
alt="ChevronDown"

View File

@@ -7,6 +7,7 @@ import {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import ArrowDown from '../assets/arrow-down.svg';
import RetryIcon from '../components/RetryIcon';
@@ -14,6 +15,7 @@ import Hero from '../Hero';
import { useDarkTheme } from '../hooks';
import ConversationBubble from './ConversationBubble';
import { FEEDBACK, Query, Status } from './conversationModels';
import { selectConversationId } from '../preferences/preferenceSlice';
const SCROLL_THRESHOLD = 10;
const LAST_BUBBLE_MARGIN = 'mb-32';
@@ -50,6 +52,7 @@ export default function ConversationMessages({
}: ConversationMessagesProps) {
const [isDarkTheme] = useDarkTheme();
const { t } = useTranslation();
const conversationId = useSelector(selectConversationId);
const conversationRef = useRef<HTMLDivElement>(null);
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
@@ -137,7 +140,7 @@ export default function ConversationMessages({
return (
<ConversationBubble
className={bubbleMargin}
key={`${index}-ANSWER`}
key={`${conversationId}-${index}-ANSWER`}
message={query.response}
type={'ANSWER'}
thought={query.thought}
@@ -175,7 +178,7 @@ export default function ConversationMessages({
return (
<ConversationBubble
className={bubbleMargin}
key={`${index}-ERROR`}
key={`${conversationId}-${index}-ERROR`}
message={query.error}
type="ERROR"
retryBtn={retryButton}
@@ -214,10 +217,10 @@ export default function ConversationMessages({
{queries.length > 0 ? (
queries.map((query, index) => (
<Fragment key={`${index}-query-fragment`}>
<Fragment key={`${conversationId}-${index}-query-fragment`}>
<ConversationBubble
className={index === 0 ? FIRST_QUESTION_BUBBLE_MARGIN_TOP : ''}
key={`${index}-QUESTION`}
key={`${conversationId}-${index}-QUESTION`}
message={query.prompt}
type="QUESTION"
handleUpdatedQuestionSubmission={handleQuestionSubmission}

View File

@@ -201,6 +201,15 @@
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
@@ -220,10 +229,14 @@
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again"
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
}
},
"scrollTabsLeft": "Scroll tabs left",
"tabsAriaLabel": "Settings tabs",
"scrollTabsRight": "Scroll tabs right"
},
"modals": {
"uploadDoc": {
@@ -343,7 +356,8 @@
"disclaimer": "This is the only time your key will be shown.",
"copy": "Copy",
"copied": "Copied",
"confirm": "I saved the Key"
"confirm": "I saved the Key",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "Are you sure you want to delete all the conversations?",
@@ -361,7 +375,8 @@
"apiKeyLabel": "API Key / OAuth",
"apiKeyPlaceholder": "Enter API Key / OAuth",
"addButton": "Add Tool",
"closeButton": "Close"
"closeButton": "Close",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "Add Prompt",
@@ -386,6 +401,22 @@
"cancel": "Cancel",
"delete": "Delete",
"deleteConfirmation": "Are you sure you want to delete this chunk?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -428,6 +459,153 @@
"attach": "Attach",
"remove": "Remove attachment"
},
"retry": "Retry"
"retry": "Retry",
"reasoning": "Reasoning"
},
"agents": {
"title": "Agents",
"description": "Discover and create custom versions of DocsGPT that combine instructions, extra knowledge, and any combination of skills",
"newAgent": "New Agent",
"backToAll": "Back to all agents",
"sections": {
"template": {
"title": "By DocsGPT",
"description": "Agents provided by DocsGPT",
"emptyState": "No template agents found."
},
"user": {
"title": "By me",
"description": "Agents created or published by you",
"emptyState": "You don't have any created agents yet."
},
"shared": {
"title": "Shared with me",
"description": "Agents imported by using a public link",
"emptyState": "No shared agents found."
}
},
"form": {
"headings": {
"new": "New Agent",
"edit": "Edit Agent",
"draft": "New Agent (Draft)"
},
"buttons": {
"publish": "Publish",
"save": "Save",
"saveDraft": "Save Draft",
"cancel": "Cancel",
"delete": "Delete",
"logs": "Logs",
"accessDetails": "Access Details",
"add": "Add"
},
"sections": {
"meta": "Meta",
"source": "Source",
"prompt": "Prompt",
"tools": "Tools",
"agentType": "Agent type",
"advanced": "Advanced",
"preview": "Preview"
},
"placeholders": {
"agentName": "Agent name",
"describeAgent": "Describe your agent",
"selectSources": "Select sources",
"chunksPerQuery": "Chunks per query",
"selectType": "Select type",
"selectTools": "Select tools",
"enterTokenLimit": "Enter token limit",
"enterRequestLimit": "Enter request limit"
},
"sourcePopup": {
"title": "Select Sources",
"searchPlaceholder": "Search sources...",
"noOptionsMessage": "No sources available"
},
"toolsPopup": {
"title": "Select Tools",
"searchPlaceholder": "Search tools...",
"noOptionsMessage": "No tools available"
},
"upload": {
"clickToUpload": "Click to upload",
"dragAndDrop": " or drag and drop"
},
"agentTypes": {
"classic": "Classic",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "JSON response schema",
"jsonSchemaDescription": "Define a JSON schema to enforce structured output format",
"validJson": "Valid JSON",
"invalidJson": "Invalid JSON - fix to enable saving",
"tokenLimiting": "Token limiting",
"tokenLimitingDescription": "Limit daily total tokens that can be used by this agent",
"requestLimiting": "Request limiting",
"requestLimitingDescription": "Limit daily total requests that can be made to this agent"
},
"preview": {
"publishedPreview": "Published agents can be previewed here"
},
"externalKb": "External KB"
},
"logs": {
"title": "Agent Logs",
"lastUsedAt": "Last used at",
"noUsageHistory": "No usage history",
"tableHeader": "Agent endpoint logs"
},
"shared": {
"notFound": "No agent found. Please ensure the agent is shared."
},
"preview": {
"testMessage": "Test your agent here. Published agents can be used in conversations."
},
"deleteConfirmation": "Are you sure you want to delete this agent?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "Search files and folders...",
"itemsSelected": "{{count}} selected",
"name": "Name",
"lastModified": "Last Modified",
"size": "Size"
},
"actionButtons": {
"openNewChat": "Open New Chat",
"share": "Share"
},
"mermaid": {
"downloadOptions": "Download options",
"viewCode": "View Code",
"decreaseZoom": "Decrease zoom",
"resetZoom": "Reset zoom",
"increaseZoom": "Increase zoom"
},
"navigation": {
"agents": "Agents"
},
"notification": {
"ariaLabel": "Notification",
"closeAriaLabel": "Close notification"
},
"prompts": {
"textAriaLabel": "Prompt Text"
}
}

View File

@@ -185,8 +185,58 @@
"fieldDescription": "Descripción del campo",
"add": "Añadir",
"cancel": "Cancelar",
"addNew": "Añadir Nuevo"
}
"addNew": "Añadir Nuevo",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing",
"saving": "Saving",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password",
"oauthScopes": "OAuth scopes (comma separated)"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
},
"scrollTabsLeft": "Desplazar pestañas a la izquierda",
"tabsAriaLabel": "Pestañas de configuración",
"scrollTabsRight": "Desplazar pestañas a la derecha"
},
"modals": {
"uploadDoc": {
@@ -306,7 +356,8 @@
"disclaimer": "Esta es la única vez que se mostrará tu clave.",
"copy": "Copiar",
"copied": "Copiado",
"confirm": "He guardado la Clave"
"confirm": "He guardado la Clave",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "¿Estás seguro de que deseas eliminar todas las conversaciones?",
@@ -324,7 +375,8 @@
"apiKeyLabel": "Clave API / OAuth",
"apiKeyPlaceholder": "Ingrese la Clave API / OAuth",
"addButton": "Agregar Herramienta",
"closeButton": "Cerrar"
"closeButton": "Cerrar",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "Agregar Prompt",
@@ -349,6 +401,22 @@
"cancel": "Cancelar",
"delete": "Eliminar",
"deleteConfirmation": "¿Estás seguro de que deseas eliminar este fragmento?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -391,6 +459,153 @@
"attach": "Adjuntar",
"remove": "Eliminar adjunto"
},
"retry": "Reintentar"
"retry": "Reintentar",
"reasoning": "Razonamiento"
},
"agents": {
"title": "Agentes",
"description": "Descubre y crea versiones personalizadas de DocsGPT que combinan instrucciones, conocimiento adicional y cualquier combinación de habilidades",
"newAgent": "Nuevo Agente",
"backToAll": "Volver a todos los agentes",
"sections": {
"template": {
"title": "Por DocsGPT",
"description": "Agentes proporcionados por DocsGPT",
"emptyState": "No se encontraron agentes de plantilla."
},
"user": {
"title": "Por mí",
"description": "Agentes creados o publicados por ti",
"emptyState": "Aún no tienes agentes creados."
},
"shared": {
"title": "Compartidos conmigo",
"description": "Agentes importados mediante un enlace público",
"emptyState": "No se encontraron agentes compartidos."
}
},
"form": {
"headings": {
"new": "Nuevo Agente",
"edit": "Editar Agente",
"draft": "Nuevo Agente (Borrador)"
},
"buttons": {
"publish": "Publicar",
"save": "Guardar",
"saveDraft": "Guardar Borrador",
"cancel": "Cancelar",
"delete": "Eliminar",
"logs": "Registros",
"accessDetails": "Detalles de Acceso",
"add": "Agregar"
},
"sections": {
"meta": "Meta",
"source": "Fuente",
"prompt": "Prompt",
"tools": "Herramientas",
"agentType": "Tipo de agente",
"advanced": "Avanzado",
"preview": "Vista previa"
},
"placeholders": {
"agentName": "Nombre del agente",
"describeAgent": "Describe tu agente",
"selectSources": "Seleccionar fuentes",
"chunksPerQuery": "Fragmentos por consulta",
"selectType": "Seleccionar tipo",
"selectTools": "Seleccionar herramientas",
"enterTokenLimit": "Ingresar límite de tokens",
"enterRequestLimit": "Ingresar límite de solicitudes"
},
"sourcePopup": {
"title": "Seleccionar Fuentes",
"searchPlaceholder": "Buscar fuentes...",
"noOptionsMessage": "No hay fuentes disponibles"
},
"toolsPopup": {
"title": "Seleccionar Herramientas",
"searchPlaceholder": "Buscar herramientas...",
"noOptionsMessage": "No hay herramientas disponibles"
},
"upload": {
"clickToUpload": "Haz clic para subir",
"dragAndDrop": " o arrastra y suelta"
},
"agentTypes": {
"classic": "Clásico",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "Esquema de respuesta JSON",
"jsonSchemaDescription": "Define un esquema JSON para aplicar formato de salida estructurado",
"validJson": "JSON válido",
"invalidJson": "JSON inválido - corrige para habilitar el guardado",
"tokenLimiting": "Límite de tokens",
"tokenLimitingDescription": "Limita el total diario de tokens que puede usar este agente",
"requestLimiting": "Límite de solicitudes",
"requestLimitingDescription": "Limita el total diario de solicitudes que se pueden hacer a este agente"
},
"preview": {
"publishedPreview": "Los agentes publicados se pueden previsualizar aquí"
},
"externalKb": "KB Externa"
},
"logs": {
"title": "Registros del Agente",
"lastUsedAt": "Último uso",
"noUsageHistory": "Sin historial de uso",
"tableHeader": "Registros del endpoint del agente"
},
"shared": {
"notFound": "No se encontró el agente. Asegúrate de que el agente esté compartido."
},
"preview": {
"testMessage": "Prueba tu agente aquí. Los agentes publicados se pueden usar en conversaciones."
},
"deleteConfirmation": "¿Estás seguro de que quieres eliminar este agente?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "Buscar archivos y carpetas...",
"itemsSelected": "{{count}} seleccionados",
"name": "Nombre",
"lastModified": "Última modificación",
"size": "Tamaño"
},
"actionButtons": {
"openNewChat": "Abrir nuevo chat",
"share": "Compartir"
},
"mermaid": {
"downloadOptions": "Opciones de descarga",
"viewCode": "Ver código",
"decreaseZoom": "Reducir zoom",
"resetZoom": "Restablecer zoom",
"increaseZoom": "Aumentar zoom"
},
"navigation": {
"agents": "Agentes"
},
"notification": {
"ariaLabel": "Notificación",
"closeAriaLabel": "Cerrar notificación"
},
"prompts": {
"textAriaLabel": "Texto del prompt"
}
}

View File

@@ -185,8 +185,58 @@
"cancel": "キャンセル",
"addNew": "新規追加",
"name": "名前",
"type": "タイプ"
}
"type": "タイプ",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing",
"saving": "Saving",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password",
"oauthScopes": "OAuth scopes (comma separated)"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
},
"scrollTabsLeft": "タブを左にスクロール",
"tabsAriaLabel": "設定タブ",
"scrollTabsRight": "タブを右にスクロール"
},
"modals": {
"uploadDoc": {
@@ -306,7 +356,8 @@
"disclaimer": "キーが表示されるのはこのときだけです。",
"copy": "コピー",
"copied": "コピーしました",
"confirm": "キーを保存しました"
"confirm": "キーを保存しました",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "すべての会話を削除してもよろしいですか?",
@@ -324,7 +375,8 @@
"apiKeyLabel": "APIキー / OAuth",
"apiKeyPlaceholder": "APIキー / OAuthを入力してください",
"addButton": "ツールを追加",
"closeButton": "閉じる"
"closeButton": "閉じる",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "プロンプトを追加",
@@ -349,6 +401,22 @@
"cancel": "キャンセル",
"delete": "削除",
"deleteConfirmation": "このチャンクを削除してもよろしいですか?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -391,6 +459,153 @@
"attach": "添付",
"remove": "添付ファイルを削除"
},
"retry": "再試行"
"retry": "再試行",
"reasoning": "推論"
},
"agents": {
"title": "エージェント",
"description": "指示、追加知識、スキルの組み合わせを含むDocsGPTのカスタムバージョンを発見して作成します",
"newAgent": "新しいエージェント",
"backToAll": "すべてのエージェントに戻る",
"sections": {
"template": {
"title": "DocsGPT提供",
"description": "DocsGPTが提供するエージェント",
"emptyState": "テンプレートエージェントが見つかりません。"
},
"user": {
"title": "自分のエージェント",
"description": "あなたが作成または公開したエージェント",
"emptyState": "まだ作成されたエージェントがありません。"
},
"shared": {
"title": "共有されたエージェント",
"description": "公開リンクを使用してインポートされたエージェント",
"emptyState": "共有エージェントが見つかりません。"
}
},
"form": {
"headings": {
"new": "新しいエージェント",
"edit": "エージェントを編集",
"draft": "新しいエージェント(下書き)"
},
"buttons": {
"publish": "公開",
"save": "保存",
"saveDraft": "下書きを保存",
"cancel": "キャンセル",
"delete": "削除",
"logs": "ログ",
"accessDetails": "アクセス詳細",
"add": "追加"
},
"sections": {
"meta": "メタ",
"source": "ソース",
"prompt": "プロンプト",
"tools": "ツール",
"agentType": "エージェントタイプ",
"advanced": "詳細設定",
"preview": "プレビュー"
},
"placeholders": {
"agentName": "エージェント名",
"describeAgent": "エージェントを説明してください",
"selectSources": "ソースを選択",
"chunksPerQuery": "クエリごとのチャンク数",
"selectType": "タイプを選択",
"selectTools": "ツールを選択",
"enterTokenLimit": "トークン制限を入力",
"enterRequestLimit": "リクエスト制限を入力"
},
"sourcePopup": {
"title": "ソースを選択",
"searchPlaceholder": "ソースを検索...",
"noOptionsMessage": "利用可能なソースがありません"
},
"toolsPopup": {
"title": "ツールを選択",
"searchPlaceholder": "ツールを検索...",
"noOptionsMessage": "利用可能なツールがありません"
},
"upload": {
"clickToUpload": "クリックしてアップロード",
"dragAndDrop": " またはドラッグ&ドロップ"
},
"agentTypes": {
"classic": "クラシック",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "JSON応答スキーマ",
"jsonSchemaDescription": "構造化された出力形式を適用するためのJSONスキーマを定義します",
"validJson": "有効なJSON",
"invalidJson": "無効なJSON - 保存を有効にするには修正してください",
"tokenLimiting": "トークン制限",
"tokenLimitingDescription": "このエージェントが使用できる1日の合計トークン数を制限します",
"requestLimiting": "リクエスト制限",
"requestLimitingDescription": "このエージェントに対して行える1日の合計リクエスト数を制限します"
},
"preview": {
"publishedPreview": "公開されたエージェントはここでプレビューできます"
},
"externalKb": "外部KB"
},
"logs": {
"title": "エージェントログ",
"lastUsedAt": "最終使用日時",
"noUsageHistory": "使用履歴がありません",
"tableHeader": "エージェントエンドポイントログ"
},
"shared": {
"notFound": "エージェントが見つかりません。エージェントが共有されていることを確認してください。"
},
"preview": {
"testMessage": "ここでエージェントをテストできます。公開されたエージェントは会話で使用できます。"
},
"deleteConfirmation": "このエージェントを削除してもよろしいですか?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "ファイルとフォルダを検索...",
"itemsSelected": "{{count}} 件選択済み",
"name": "名前",
"lastModified": "最終更新日",
"size": "サイズ"
},
"actionButtons": {
"openNewChat": "新しいチャットを開く",
"share": "共有"
},
"mermaid": {
"downloadOptions": "ダウンロードオプション",
"viewCode": "コードを表示",
"decreaseZoom": "ズームアウト",
"resetZoom": "ズームをリセット",
"increaseZoom": "ズームイン"
},
"navigation": {
"agents": "エージェント"
},
"notification": {
"ariaLabel": "通知",
"closeAriaLabel": "通知を閉じる"
},
"prompts": {
"textAriaLabel": "プロンプトテキスト"
}
}

View File

@@ -185,8 +185,58 @@
"cancel": "Отмена",
"addNew": "Добавить новое",
"name": "Имя",
"type": "Тип"
}
"type": "Тип",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing",
"saving": "Saving",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password",
"oauthScopes": "OAuth scopes (comma separated)"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
},
"scrollTabsLeft": "Прокрутить вкладки влево",
"tabsAriaLabel": "Вкладки настроек",
"scrollTabsRight": "Прокрутить вкладки вправо"
},
"modals": {
"uploadDoc": {
@@ -306,7 +356,8 @@
"disclaimer": "Ваш ключ будет показан только один раз.",
"copy": "Копировать",
"copied": "Скопировано",
"confirm": "Я сохранил ключ"
"confirm": "Я сохранил ключ",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "Вы уверены, что хотите удалить все разговоры?",
@@ -324,7 +375,8 @@
"apiKeyLabel": "API ключ / OAuth",
"apiKeyPlaceholder": "Введите API ключ / OAuth",
"addButton": "Добавить инструмент",
"closeButton": "Закрыть"
"closeButton": "Закрыть",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "Добавить подсказку",
@@ -349,6 +401,22 @@
"cancel": "Отмена",
"delete": "Удалить",
"deleteConfirmation": "Вы уверены, что хотите удалить этот фрагмент?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -391,6 +459,153 @@
"attach": "Прикрепить",
"remove": "Удалить вложение"
},
"retry": "Повторить"
"retry": "Повторить",
"reasoning": "Рассуждение"
},
"agents": {
"title": "Агенты",
"description": "Откройте и создайте пользовательские версии DocsGPT, которые объединяют инструкции, дополнительные знания и любую комбинацию навыков",
"newAgent": "Новый Агент",
"backToAll": "Вернуться ко всем агентам",
"sections": {
"template": {
"title": "От DocsGPT",
"description": "Агенты, предоставленные DocsGPT",
"emptyState": "Шаблонные агенты не найдены."
},
"user": {
"title": "Мои",
"description": "Агенты, созданные или опубликованные вами",
"emptyState": "У вас пока нет созданных агентов."
},
"shared": {
"title": "Поделились со мной",
"description": "Агенты, импортированные по публичной ссылке",
"emptyState": "Общие агенты не найдены."
}
},
"form": {
"headings": {
"new": "Новый Агент",
"edit": "Редактировать Агента",
"draft": "Новый Агент (Черновик)"
},
"buttons": {
"publish": "Опубликовать",
"save": "Сохранить",
"saveDraft": "Сохранить Черновик",
"cancel": "Отмена",
"delete": "Удалить",
"logs": "Логи",
"accessDetails": "Детали Доступа",
"add": "Добавить"
},
"sections": {
"meta": "Мета",
"source": "Источник",
"prompt": "Промпт",
"tools": "Инструменты",
"agentType": "Тип агента",
"advanced": "Расширенные",
"preview": "Предпросмотр"
},
"placeholders": {
"agentName": "Имя агента",
"describeAgent": "Опишите вашего агента",
"selectSources": "Выберите источники",
"chunksPerQuery": "Фрагментов на запрос",
"selectType": "Выберите тип",
"selectTools": "Выберите инструменты",
"enterTokenLimit": "Введите лимит токенов",
"enterRequestLimit": "Введите лимит запросов"
},
"sourcePopup": {
"title": "Выберите Источники",
"searchPlaceholder": "Поиск источников...",
"noOptionsMessage": "Нет доступных источников"
},
"toolsPopup": {
"title": "Выберите Инструменты",
"searchPlaceholder": "Поиск инструментов...",
"noOptionsMessage": "Нет доступных инструментов"
},
"upload": {
"clickToUpload": "Нажмите для загрузки",
"dragAndDrop": " или перетащите"
},
"agentTypes": {
"classic": "Классический",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "Схема ответа JSON",
"jsonSchemaDescription": "Определите схему JSON для применения структурированного формата вывода",
"validJson": "Валидный JSON",
"invalidJson": "Невалидный JSON - исправьте для сохранения",
"tokenLimiting": "Лимит токенов",
"tokenLimitingDescription": "Ограничить ежедневное общее количество токенов, которые может использовать этот агент",
"requestLimiting": "Лимит запросов",
"requestLimitingDescription": "Ограничить ежедневное общее количество запросов, которые можно сделать к этому агенту"
},
"preview": {
"publishedPreview": "Опубликованные агенты можно просмотреть здесь"
},
"externalKb": "Внешняя БЗ"
},
"logs": {
"title": "Логи Агента",
"lastUsedAt": "Последнее использование",
"noUsageHistory": "Нет истории использования",
"tableHeader": "Логи конечной точки агента"
},
"shared": {
"notFound": "Агент не найден. Убедитесь, что агент является общим."
},
"preview": {
"testMessage": "Протестируйте своего агента здесь. Опубликованные агенты можно использовать в разговорах."
},
"deleteConfirmation": "Вы уверены, что хотите удалить этого агента?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "Поиск файлов и папок...",
"itemsSelected": "{{count}} выбрано",
"name": "Имя",
"lastModified": "Последнее изменение",
"size": "Размер"
},
"actionButtons": {
"openNewChat": "Открыть новый чат",
"share": "Поделиться"
},
"mermaid": {
"downloadOptions": "Параметры загрузки",
"viewCode": "Просмотр кода",
"decreaseZoom": "Уменьшить масштаб",
"resetZoom": "Сбросить масштаб",
"increaseZoom": "Увеличить масштаб"
},
"navigation": {
"agents": "Агенты"
},
"notification": {
"ariaLabel": "Уведомление",
"closeAriaLabel": "Закрыть уведомление"
},
"prompts": {
"textAriaLabel": "Текст подсказки"
}
}

View File

@@ -185,8 +185,58 @@
"cancel": "取消",
"addNew": "新增",
"name": "名稱",
"type": "類型"
}
"type": "類型",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing",
"saving": "Saving",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password",
"oauthScopes": "OAuth scopes (comma separated)"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
},
"scrollTabsLeft": "向左捲動標籤",
"tabsAriaLabel": "設定標籤",
"scrollTabsRight": "向右捲動標籤"
},
"modals": {
"uploadDoc": {
@@ -306,7 +356,8 @@
"disclaimer": "這是唯一一次顯示您的金鑰。",
"copy": "複製",
"copied": "已複製",
"confirm": "我已儲存金鑰"
"confirm": "我已儲存金鑰",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "您確定要刪除所有對話嗎?",
@@ -324,7 +375,8 @@
"apiKeyLabel": "API 金鑰 / OAuth",
"apiKeyPlaceholder": "輸入 API 金鑰 / OAuth",
"addButton": "新增工具",
"closeButton": "關閉"
"closeButton": "關閉",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "新增提示",
@@ -349,6 +401,22 @@
"cancel": "取消",
"delete": "刪除",
"deleteConfirmation": "您確定要刪除此區塊嗎?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -391,6 +459,153 @@
"attach": "附件",
"remove": "刪除附件"
},
"retry": "重試"
"retry": "重試",
"reasoning": "推理"
},
"agents": {
"title": "代理",
"description": "探索並創建結合指令、額外知識和任意技能組合的DocsGPT自訂版本",
"newAgent": "新建代理",
"backToAll": "返回所有代理",
"sections": {
"template": {
"title": "由DocsGPT提供",
"description": "DocsGPT提供的代理",
"emptyState": "未找到範本代理。"
},
"user": {
"title": "我的代理",
"description": "您創建或發佈的代理",
"emptyState": "您還沒有創建任何代理。"
},
"shared": {
"title": "與我共享",
"description": "透過公共連結匯入的代理",
"emptyState": "未找到共享代理。"
}
},
"form": {
"headings": {
"new": "新建代理",
"edit": "編輯代理",
"draft": "新建代理(草稿)"
},
"buttons": {
"publish": "發佈",
"save": "儲存",
"saveDraft": "儲存草稿",
"cancel": "取消",
"delete": "刪除",
"logs": "日誌",
"accessDetails": "存取詳情",
"add": "新增"
},
"sections": {
"meta": "中繼資料",
"source": "來源",
"prompt": "提示詞",
"tools": "工具",
"agentType": "代理類型",
"advanced": "進階",
"preview": "預覽"
},
"placeholders": {
"agentName": "代理名稱",
"describeAgent": "描述您的代理",
"selectSources": "選擇來源",
"chunksPerQuery": "每次查詢的區塊數",
"selectType": "選擇類型",
"selectTools": "選擇工具",
"enterTokenLimit": "輸入權杖限制",
"enterRequestLimit": "輸入請求限制"
},
"sourcePopup": {
"title": "選擇來源",
"searchPlaceholder": "搜尋來源...",
"noOptionsMessage": "沒有可用的來源"
},
"toolsPopup": {
"title": "選擇工具",
"searchPlaceholder": "搜尋工具...",
"noOptionsMessage": "沒有可用的工具"
},
"upload": {
"clickToUpload": "點擊上傳",
"dragAndDrop": " 或拖放"
},
"agentTypes": {
"classic": "經典",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "JSON回應架構",
"jsonSchemaDescription": "定義JSON架構以強制執行結構化輸出格式",
"validJson": "有效的JSON",
"invalidJson": "無效的JSON - 修復後才能儲存",
"tokenLimiting": "權杖限制",
"tokenLimitingDescription": "限制此代理每天可使用的總權杖數",
"requestLimiting": "請求限制",
"requestLimitingDescription": "限制每天可向此代理發出的總請求數"
},
"preview": {
"publishedPreview": "已發佈的代理可以在此處預覽"
},
"externalKb": "外部知識庫"
},
"logs": {
"title": "代理日誌",
"lastUsedAt": "最後使用時間",
"noUsageHistory": "無使用歷史",
"tableHeader": "代理端點日誌"
},
"shared": {
"notFound": "未找到代理。請確保代理已共享。"
},
"preview": {
"testMessage": "在此測試您的代理。已發佈的代理可以在對話中使用。"
},
"deleteConfirmation": "您確定要刪除此代理嗎?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "搜尋檔案和資料夾...",
"itemsSelected": "已選擇 {{count}} 項",
"name": "名稱",
"lastModified": "最後修改",
"size": "大小"
},
"actionButtons": {
"openNewChat": "開啟新聊天",
"share": "分享"
},
"mermaid": {
"downloadOptions": "下載選項",
"viewCode": "查看程式碼",
"decreaseZoom": "縮小",
"resetZoom": "重設縮放",
"increaseZoom": "放大"
},
"navigation": {
"agents": "代理"
},
"notification": {
"ariaLabel": "通知",
"closeAriaLabel": "關閉通知"
},
"prompts": {
"textAriaLabel": "提示文字"
}
}

View File

@@ -185,8 +185,58 @@
"cancel": "取消",
"addNew": "添加新的",
"name": "名称",
"type": "类型"
}
"type": "类型",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing",
"saving": "Saving",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"oauthInProgress": "Waiting for OAuth completion...",
"oauthCompleted": "OAuth completed successfully",
"authType": "Authentication Type",
"defaultServerName": "My MCP Server",
"authTypes": {
"none": "No Authentication",
"apiKey": "API Key",
"bearer": "Bearer Token",
"oauth": "OAuth",
"basic": "Basic Authentication"
},
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password",
"oauthScopes": "OAuth scopes (comma separated)"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server",
"oauthFailed": "OAuth process failed or was cancelled",
"oauthTimeout": "OAuth process timed out, please try again",
"timeoutRange": "Timeout must be between 1 and 300 seconds"
}
}
},
"scrollTabsLeft": "向左滚动标签",
"tabsAriaLabel": "设置标签",
"scrollTabsRight": "向右滚动标签"
},
"modals": {
"uploadDoc": {
@@ -306,7 +356,8 @@
"disclaimer": "这是您的密钥唯一一次展示机会。",
"copy": "复制",
"copied": "已复制",
"confirm": "我已保存密钥"
"confirm": "我已保存密钥",
"apiKeyLabel": "API Key"
},
"deleteConv": {
"confirm": "您确定要删除所有对话吗?",
@@ -324,7 +375,8 @@
"apiKeyLabel": "API 密钥 / OAuth",
"apiKeyPlaceholder": "输入 API 密钥 / OAuth",
"addButton": "添加工具",
"closeButton": "关闭"
"closeButton": "关闭",
"customNamePlaceholder": "Enter custom name (optional)"
},
"prompts": {
"addPrompt": "添加提示",
@@ -349,6 +401,22 @@
"cancel": "取消",
"delete": "删除",
"deleteConfirmation": "您确定要删除此块吗?"
},
"addAction": {
"title": "New Action",
"actionNamePlaceholder": "Action Name",
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
"addButton": "Add"
},
"agentDetails": {
"title": "Access Details",
"publicLink": "Public Link",
"apiKey": "API Key",
"webhookUrl": "Webhook URL",
"generate": "Generate",
"test": "Test",
"learnMore": "Learn more"
}
},
"sharedConv": {
@@ -391,6 +459,153 @@
"attach": "附件",
"remove": "删除附件"
},
"retry": "重试"
"retry": "重试",
"reasoning": "推理"
},
"agents": {
"title": "代理",
"description": "发现并创建结合指令、额外知识和任意技能组合的DocsGPT自定义版本",
"newAgent": "新建代理",
"backToAll": "返回所有代理",
"sections": {
"template": {
"title": "由DocsGPT提供",
"description": "DocsGPT提供的代理",
"emptyState": "未找到模板代理。"
},
"user": {
"title": "我的代理",
"description": "您创建或发布的代理",
"emptyState": "您还没有创建任何代理。"
},
"shared": {
"title": "与我共享",
"description": "通过公共链接导入的代理",
"emptyState": "未找到共享代理。"
}
},
"form": {
"headings": {
"new": "新建代理",
"edit": "编辑代理",
"draft": "新建代理(草稿)"
},
"buttons": {
"publish": "发布",
"save": "保存",
"saveDraft": "保存草稿",
"cancel": "取消",
"delete": "删除",
"logs": "日志",
"accessDetails": "访问详情",
"add": "添加"
},
"sections": {
"meta": "元数据",
"source": "来源",
"prompt": "提示词",
"tools": "工具",
"agentType": "代理类型",
"advanced": "高级",
"preview": "预览"
},
"placeholders": {
"agentName": "代理名称",
"describeAgent": "描述您的代理",
"selectSources": "选择来源",
"chunksPerQuery": "每次查询的块数",
"selectType": "选择类型",
"selectTools": "选择工具",
"enterTokenLimit": "输入令牌限制",
"enterRequestLimit": "输入请求限制"
},
"sourcePopup": {
"title": "选择来源",
"searchPlaceholder": "搜索来源...",
"noOptionsMessage": "没有可用的来源"
},
"toolsPopup": {
"title": "选择工具",
"searchPlaceholder": "搜索工具...",
"noOptionsMessage": "没有可用的工具"
},
"upload": {
"clickToUpload": "点击上传",
"dragAndDrop": " 或拖放"
},
"agentTypes": {
"classic": "经典",
"react": "ReAct"
},
"advanced": {
"jsonSchema": "JSON响应架构",
"jsonSchemaDescription": "定义JSON架构以强制执行结构化输出格式",
"validJson": "有效的JSON",
"invalidJson": "无效的JSON - 修复后才能保存",
"tokenLimiting": "令牌限制",
"tokenLimitingDescription": "限制此代理每天可使用的总令牌数",
"requestLimiting": "请求限制",
"requestLimitingDescription": "限制每天可向此代理发出的总请求数"
},
"preview": {
"publishedPreview": "已发布的代理可以在此处预览"
},
"externalKb": "外部知识库"
},
"logs": {
"title": "代理日志",
"lastUsedAt": "最后使用时间",
"noUsageHistory": "无使用历史",
"tableHeader": "代理端点日志"
},
"shared": {
"notFound": "未找到代理。请确保代理已共享。"
},
"preview": {
"testMessage": "在此测试您的代理。已发布的代理可以在对话中使用。"
},
"deleteConfirmation": "您确定要删除此代理吗?"
},
"components": {
"fileUpload": {
"clickToUpload": "Click to upload or drag and drop",
"dropFiles": "Drop the files here",
"fileTypes": "PNG, JPG, JPEG up to",
"sizeLimitUnit": "MB",
"fileSizeError": "File exceeds {{size}}MB limit"
}
},
"pageNotFound": {
"title": "404",
"message": "The page you are looking for does not exist.",
"goHome": "Go Back Home"
},
"filePicker": {
"searchPlaceholder": "搜索文件和文件夹...",
"itemsSelected": "已选择 {{count}} 项",
"name": "名称",
"lastModified": "最后修改",
"size": "大小"
},
"actionButtons": {
"openNewChat": "打开新聊天",
"share": "分享"
},
"mermaid": {
"downloadOptions": "下载选项",
"viewCode": "查看代码",
"decreaseZoom": "缩小",
"resetZoom": "重置缩放",
"increaseZoom": "放大"
},
"navigation": {
"agents": "代理"
},
"notification": {
"ariaLabel": "通知",
"closeAriaLabel": "关闭通知"
},
"prompts": {
"textAriaLabel": "提示文本"
}
}

View File

@@ -44,7 +44,7 @@ export default function AddActionModal({
>
<div>
<h2 className="text-jet dark:text-bright-gray px-3 text-xl font-semibold">
New Action
{t('modals.addAction.title')}
</h2>
<div className="relative mt-6 px-3">
<Input
@@ -57,7 +57,7 @@ export default function AddActionModal({
}}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
placeholder="Action Name"
placeholder={t('modals.addAction.actionNamePlaceholder')}
required={true}
/>
<p
@@ -66,8 +66,8 @@ export default function AddActionModal({
}`}
>
{functionNameError
? 'Invalid function name format. Use only letters, numbers, underscores, and hyphens.'
: 'Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)'}
? t('modals.addAction.invalidFormat')
: t('modals.addAction.formatHelp')}
</p>
</div>
<div className="mt-3 flex flex-row-reverse gap-1 px-3">
@@ -75,7 +75,7 @@ export default function AddActionModal({
onClick={handleAddAction}
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
>
Add
{t('modals.addAction.addButton')}
</button>
<button
onClick={() => {

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Agent } from '../agents/types';
@@ -24,6 +25,7 @@ export default function AgentDetailsModal({
modalState,
setModalState,
}: AgentDetailsModalProps) {
const { t } = useTranslation();
const token = useSelector(selectToken);
const [sharedToken, setSharedToken] = useState<string | null>(
@@ -86,13 +88,13 @@ export default function AgentDetailsModal({
>
<div>
<h2 className="text-jet dark:text-bright-gray text-xl font-semibold">
Access Details
{t('modals.agentDetails.title')}
</h2>
<div className="mt-8 flex flex-col gap-6">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
Public Link
{t('modals.agentDetails.publicLink')}
</h2>
</div>
{sharedToken ? (
@@ -117,7 +119,9 @@ export default function AgentDetailsModal({
target="_blank"
rel="noopener noreferrer"
>
<span className="text-sm">Learn more</span>
<span className="text-sm">
{t('modals.agentDetails.learnMore')}
</span>
<img
src="/src/assets/external-link.svg"
alt="External link"
@@ -133,14 +137,14 @@ export default function AgentDetailsModal({
{loadingStates.publicLink ? (
<Spinner size="small" color="#976af3" />
) : (
'Generate'
t('modals.agentDetails.generate')
)}
</button>
)}
</div>
<div className="flex flex-col gap-3">
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
API Key
{t('modals.agentDetails.apiKey')}
</h2>
{apiKey ? (
<div className="flex flex-col gap-2">
@@ -162,7 +166,7 @@ export default function AgentDetailsModal({
target="_blank"
rel="noopener noreferrer"
>
Test
{t('modals.agentDetails.test')}
<img
src="/src/assets/external-link.svg"
alt="External link"
@@ -174,14 +178,14 @@ export default function AgentDetailsModal({
</div>
) : (
<button className="border-purple-30 text-purple-30 hover:bg-purple-30 w-28 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white">
Generate
{t('modals.agentDetails.generate')}
</button>
)}
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
Webhook URL
{t('modals.agentDetails.webhookUrl')}
</h2>
</div>
{webhookUrl ? (
@@ -202,7 +206,9 @@ export default function AgentDetailsModal({
target="_blank"
rel="noopener noreferrer"
>
<span className="text-sm">Learn more</span>
<span className="text-sm">
{t('modals.agentDetails.learnMore')}
</span>
<img
src="/src/assets/external-link.svg"
alt="External link"
@@ -218,7 +224,7 @@ export default function AgentDetailsModal({
{loadingStates.webhook ? (
<Spinner size="small" color="#976af3" />
) : (
'Generate'
t('modals.agentDetails.generate')
)}
</button>
)}

View File

@@ -66,7 +66,7 @@ export default function ConfigToolModal({
value={customName}
onChange={(e) => setCustomName(e.target.value)}
borderVariant="thin"
placeholder="Enter custom name (optional)"
placeholder={t('modals.configTool.customNamePlaceholder')}
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>

View File

@@ -1,155 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import { CreateAPIKeyModalProps, Doc } from '../models/misc';
import { selectSourceDocs, selectToken } from '../preferences/preferenceSlice';
import WrapperModal from './WrapperModal';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
export default function CreateAPIKeyModal({
close,
createAPIKey,
}: CreateAPIKeyModalProps) {
const { t } = useTranslation();
const token = useSelector(selectToken);
const docs = useSelector(selectSourceDocs);
const [APIKeyName, setAPIKeyName] = React.useState<string>('');
const [sourcePath, setSourcePath] = React.useState<{
name: string;
id: string;
type: string;
} | null>(null);
const [prompt, setPrompt] = React.useState<{
name: string;
id: string;
type: string;
} | null>(null);
const [activePrompts, setActivePrompts] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const [chunk, setChunk] = React.useState<string>('2');
const chunkOptions = ['0', '2', '4', '6', '8', '10'];
const extractDocPaths = () =>
docs
? docs
.filter((doc) => doc.model === embeddingsName)
.map((doc: Doc) => {
if ('id' in doc) {
return {
name: doc.name,
id: doc.id as string,
type: 'local',
};
}
return {
name: doc.name,
id: doc.id ?? 'default',
type: doc.type ?? 'default',
};
})
: [];
React.useEffect(() => {
const handleFetchPrompts = async () => {
try {
const response = await userService.getPrompts(token);
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const promptsData = await response.json();
setActivePrompts(promptsData);
} catch (error) {
console.error(error);
}
};
handleFetchPrompts();
}, []);
return (
<WrapperModal close={close} className="p-4">
<div className="mb-6">
<span className="text-jet dark:text-bright-gray text-xl">
{t('modals.createAPIKey.label')}
</span>
</div>
<div className="relative mt-5 mb-4">
<Input
type="text"
className="rounded-md"
value={APIKeyName}
placeholder={t('modals.createAPIKey.apiKeyName')}
onChange={(e) => setAPIKeyName(e.target.value)}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
></Input>
</div>
<div className="my-4">
<Dropdown
placeholder={t('modals.createAPIKey.sourceDoc')}
selectedValue={sourcePath ? sourcePath.name : null}
onSelect={(selection: { name: string; id: string; type: string }) => {
setSourcePath(selection);
}}
options={extractDocPaths()}
size="w-full"
rounded="xl"
border="border"
/>
</div>
<div className="my-4">
<Dropdown
options={activePrompts}
selectedValue={prompt ? prompt.name : null}
placeholder={t('modals.createAPIKey.prompt')}
onSelect={(value: { name: string; id: string; type: string }) =>
setPrompt(value)
}
size="w-full"
border="border"
/>
</div>
<div className="my-4">
<p className="text-jet dark:text-bright-gray mb-2 ml-2 font-semibold">
{t('modals.createAPIKey.chunks')}
</p>
<Dropdown
options={chunkOptions}
selectedValue={chunk}
onSelect={(value: string) => setChunk(value)}
size="w-full"
border="border"
/>
</div>
<button
disabled={!sourcePath || APIKeyName.length === 0 || !prompt}
onClick={() => {
if (sourcePath && prompt) {
const payload: any = {
name: APIKeyName,
prompt_id: prompt.id,
chunks: chunk,
};
if (sourcePath.type === 'default') {
payload.retriever = sourcePath.id;
}
if (sourcePath.type === 'local') {
payload.source = sourcePath.id;
}
createAPIKey(payload);
}
}}
className="bg-purple-30 hover:bg-violets-are-blue float-right mt-4 rounded-full px-5 py-2 text-sm text-white disabled:opacity-50"
>
{t('modals.createAPIKey.create')}
</button>
</WrapperModal>
);
}

View File

@@ -38,7 +38,7 @@ export default function DeleteConvModal({
<ConfirmationModal
message={t('modals.deleteConv.confirm')}
modalState={modalState}
setModalState={setModalState}
setModalState={(state) => dispatch(setModalState(state))}
submitLabel={t('modals.deleteConv.delete')}
handleSubmit={handleSubmit}
handleCancel={handleCancel}

View File

@@ -18,14 +18,6 @@ interface MCPServerModalProps {
onServerSaved: () => void;
}
const authTypes = [
{ label: 'No Authentication', value: 'none' },
{ label: 'API Key', value: 'api_key' },
{ label: 'Bearer Token', value: 'bearer' },
{ label: 'OAuth', value: 'oauth' },
// { label: 'Basic Authentication', value: 'basic' },
];
export default function MCPServerModal({
modalState,
setModalState,
@@ -36,8 +28,16 @@ export default function MCPServerModal({
const token = useSelector(selectToken);
const modalRef = useRef<HTMLDivElement>(null);
const authTypes = [
{ label: t('settings.tools.mcp.authTypes.none'), value: 'none' },
{ label: t('settings.tools.mcp.authTypes.apiKey'), value: 'api_key' },
{ label: t('settings.tools.mcp.authTypes.bearer'), value: 'bearer' },
{ label: t('settings.tools.mcp.authTypes.oauth'), value: 'oauth' },
// { label: t('settings.tools.mcp.authTypes.basic'), value: 'basic' },
];
const [formData, setFormData] = useState({
name: server?.displayName || 'My MCP Server',
name: server?.displayName || t('settings.tools.mcp.defaultServerName'),
server_url: server?.server_url || '',
auth_type: server?.auth_type || 'none',
api_key: '',
@@ -72,7 +72,7 @@ export default function MCPServerModal({
const resetForm = () => {
setFormData({
name: 'My MCP Server',
name: t('settings.tools.mcp.defaultServerName'),
server_url: '',
auth_type: 'none',
api_key: '',
@@ -133,7 +133,7 @@ export default function MCPServerModal({
typeof timeoutValue === 'number' &&
(timeoutValue < 1 || timeoutValue > 300)
)
newErrors.timeout = 'Timeout must be between 1 and 300 seconds';
newErrors.timeout = t('settings.tools.mcp.errors.timeoutRange');
if (authFieldChecks[formData.auth_type])
authFieldChecks[formData.auth_type]();

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SaveAPIKeyModalProps } from '../models/misc';
import WrapperModal from './WrapperModal';
export default function SaveAPIKeyModal({
apiKey,
close,
}: SaveAPIKeyModalProps) {
const { t } = useTranslation();
const [isCopied, setIsCopied] = React.useState(false);
const handleCopyKey = () => {
navigator.clipboard.writeText(apiKey);
setIsCopied(true);
};
return (
<WrapperModal close={close}>
<h1 className="text-jet dark:text-bright-gray my-0 text-xl font-medium">
{t('modals.saveKey.note')}
</h1>
<h3 className="text-outer-space dark:text-silver text-sm font-normal">
{t('modals.saveKey.disclaimer')}
</h3>
<div className="flex justify-between py-2">
<div>
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
API Key
</h2>
<span className="text-jet dark:text-bright-gray text-sm leading-7 font-normal">
{apiKey}
</span>
</div>
<button
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue my-1 h-10 w-20 rounded-full border border-solid p-2 text-sm hover:text-white"
onClick={handleCopyKey}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
</button>
</div>
<button
onClick={close}
className="bg-philippine-yellow rounded-full px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
>
{t('modals.saveKey.confirm')}
</button>
</WrapperModal>
);
}

View File

@@ -39,18 +39,3 @@ export type DocumentsProps = {
paginatedDocuments: Doc[] | null;
handleDeleteDocument: (index: number, document: Doc) => void;
};
export type CreateAPIKeyModalProps = {
close: () => void;
createAPIKey: (payload: {
name: string;
source: string;
prompt_id: string;
chunks: string;
}) => void;
};
export type SaveAPIKeyModalProps = {
apiKey: string;
close: () => void;
};

View File

@@ -54,7 +54,7 @@ function AddPrompt({
className="border-silver dark:border-silver/40 h-56 w-full resize-none rounded-lg border-2 px-3 py-2 outline-hidden dark:bg-transparent dark:text-white"
value={newPromptContent}
onChange={(e) => setNewPromptContent(e.target.value)}
aria-label="Prompt Text"
aria-label={t('prompts.textAriaLabel')}
></textarea>
</div>
<div className="mt-6 flex flex-row-reverse">
@@ -126,7 +126,7 @@ function EditPrompt({
className="border-silver dark:border-silver/40 h-56 w-full resize-none rounded-lg border-2 px-3 py-2 outline-hidden dark:bg-transparent dark:text-white"
value={editPromptContent}
onChange={(e) => setEditPromptContent(e.target.value)}
aria-label="Prompt Text"
aria-label={t('prompts.textAriaLabel')}
></textarea>
</div>
<div className="mt-6 flex flex-row-reverse gap-4">

View File

@@ -90,9 +90,27 @@ export function getLocalApiKey(): string | null {
return key;
}
export function getLocalRecentDocs(): Doc[] | null {
const docs = localStorage.getItem('DocsGPTRecentDocs');
return docs ? (JSON.parse(docs) as Doc[]) : null;
export function getLocalRecentDocs(sourceDocs?: Doc[] | null): Doc[] | null {
const docsString = localStorage.getItem('DocsGPTRecentDocs');
const selectedDocs = docsString ? (JSON.parse(docsString) as Doc[]) : null;
if (!sourceDocs || !selectedDocs || selectedDocs.length === 0) {
return selectedDocs;
}
const isDocAvailable = (selected: Doc) => {
return sourceDocs.some((source) => {
if (source.id && selected.id) {
return source.id === selected.id;
}
return source.name === selected.name && source.date === selected.date;
});
};
const validDocs = selectedDocs.filter(isDocAvailable);
setLocalRecentDocs(validDocs.length > 0 ? validDocs : null);
return validDocs.length > 0 ? validDocs : null;
}
export function getLocalPrompt(): string | null {

View File

@@ -8,7 +8,11 @@ import {
import { Agent } from '../agents/types';
import { ActiveState, Doc } from '../models/misc';
import { RootState } from '../store';
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
import {
setLocalApiKey,
setLocalRecentDocs,
getLocalRecentDocs,
} from './preferenceApi';
export interface Preference {
apiKey: string;
@@ -178,6 +182,22 @@ prefListenerMiddleware.startListening({
},
});
prefListenerMiddleware.startListening({
matcher: isAnyOf(setSourceDocs),
effect: (_action, listenerApi) => {
const state = listenerApi.getState() as RootState;
const sourceDocs = state.preference.sourceDocs;
if (sourceDocs && sourceDocs.length > 0) {
const validatedDocs = getLocalRecentDocs(sourceDocs);
if (validatedDocs !== null) {
listenerApi.dispatch(setSelectedDocs(validatedDocs));
} else {
listenerApi.dispatch(setSelectedDocs([]));
}
}
},
});
export const selectApiKey = (state: RootState) => state.preference.apiKey;
export const selectApiKeyStatus = (state: RootState) =>
!!state.preference.apiKey;

View File

@@ -27,8 +27,8 @@ import {
} from '../preferences/preferenceSlice';
import Upload from '../upload/Upload';
import { formatDate } from '../utils/dateTimeUtils';
import FileTreeComponent from '../components/FileTreeComponent';
import ConnectorTreeComponent from '../components/ConnectorTreeComponent';
import FileTree from '../components/FileTree';
import ConnectorTree from '../components/ConnectorTree';
import Chunks from '../components/Chunks';
const formatTokens = (tokens: number): string => {
@@ -273,13 +273,13 @@ export default function Sources({
<div className="mt-8 flex flex-col">
{documentToView.isNested ? (
documentToView.type === 'connector:file' ? (
<ConnectorTreeComponent
<ConnectorTree
docId={documentToView.id || ''}
sourceName={documentToView.name}
onBackToDocuments={() => setDocumentToView(undefined)}
/>
) : (
<FileTreeComponent
<FileTree
docId={documentToView.id || ''}
sourceName={documentToView.name}
onBackToDocuments={() => setDocumentToView(undefined)}

View File

@@ -192,7 +192,7 @@ export default function Tools() {
<div className="flex w-full flex-col items-center justify-center py-12">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt="No tools found"
alt={t('settings.tools.noToolsFound')}
className="mx-auto mb-6 h-32 w-32"
/>
<p className="text-center text-lg text-gray-500 dark:text-gray-400">