mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-28 03:30:06 +00:00
846 feature request onboarding flow Redesign (#1058)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Co-authored-by: 4pmtong <web_chentong@163.com>
This commit is contained in:
parent
09200a8cf6
commit
77112a227d
54 changed files with 4358 additions and 4008 deletions
|
|
@ -6,6 +6,7 @@ package/@stackframe/
|
|||
dist/
|
||||
dist-electron/
|
||||
build/
|
||||
release/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
|
|
|
|||
|
|
@ -94,6 +94,18 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"dmg": {
|
||||
"background": "src/assets/dmg-background.png",
|
||||
"window": {
|
||||
"width": 660,
|
||||
"height": 400
|
||||
},
|
||||
"iconSize": 128,
|
||||
"contents": [
|
||||
{ "x": 180, "y": 200, "type": "file" },
|
||||
{ "x": 480, "y": 200, "type": "link", "path": "/Applications" }
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"icon": "build/icon.ico",
|
||||
"artifactName": "${productName}.Setup.${version}.exe",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export default [
|
|||
'dist/**',
|
||||
'dist-electron/**',
|
||||
'build/**',
|
||||
'release/**',
|
||||
// Cache
|
||||
'.cache/**',
|
||||
'.vite/**',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
/* global console process */
|
||||
/**
|
||||
* Comprehensive test for prebuilt dependencies
|
||||
* Verifies pyvenv.cfg and Python symlinks are correct
|
||||
|
|
@ -96,7 +97,7 @@ function testPythonSymlinks(venvPath, venvName) {
|
|||
fail(` Target: ${target}`);
|
||||
warn('Run: npm run fix-symlinks');
|
||||
} catch (err) {
|
||||
fail(`${symlinkName}: missing`);
|
||||
fail(`${symlinkName}: missing - ${err.message}`);
|
||||
warn('Run: npm run fix-symlinks');
|
||||
}
|
||||
continue;
|
||||
|
|
@ -172,12 +173,12 @@ function main() {
|
|||
const venvs = [
|
||||
{
|
||||
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
|
||||
name: 'Backend venv'
|
||||
name: 'Backend venv',
|
||||
},
|
||||
{
|
||||
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
|
||||
name: 'Terminal venv'
|
||||
}
|
||||
name: 'Terminal venv',
|
||||
},
|
||||
];
|
||||
|
||||
for (const venv of venvs) {
|
||||
|
|
@ -193,7 +194,9 @@ function main() {
|
|||
}
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log(`\n📊 Test Results: ${testsPassed} passed, ${testsFailed} failed`);
|
||||
console.log(
|
||||
`\n📊 Test Results: ${testsPassed} passed, ${testsFailed} failed`
|
||||
);
|
||||
|
||||
if (testsFailed > 0) {
|
||||
console.log('\n❌ Some tests failed!');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
/* global console process */
|
||||
/**
|
||||
* Test script to verify pyvenv.cfg fix works correctly
|
||||
*/
|
||||
|
|
@ -17,12 +18,24 @@ function testVenvFix() {
|
|||
const testCases = [
|
||||
{
|
||||
name: 'Backend venv',
|
||||
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv', 'pyvenv.cfg')
|
||||
path: path.join(
|
||||
projectRoot,
|
||||
'resources',
|
||||
'prebuilt',
|
||||
'venv',
|
||||
'pyvenv.cfg'
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Terminal venv',
|
||||
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv', 'pyvenv.cfg')
|
||||
}
|
||||
path: path.join(
|
||||
projectRoot,
|
||||
'resources',
|
||||
'prebuilt',
|
||||
'terminal_venv',
|
||||
'pyvenv.cfg'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
|
|
@ -52,12 +65,15 @@ function testVenvFix() {
|
|||
console.log(` Home: ${homePath}`);
|
||||
|
||||
// Verify placeholder format (accept both / and \ for cross-platform)
|
||||
const expectedPattern = /^\{\{PREBUILT_PYTHON_DIR\}\}[\/\\]cpython-[\w\.\-]+[\/\\](bin|Scripts)$/;
|
||||
const expectedPattern =
|
||||
/^\{\{PREBUILT_PYTHON_DIR\}\}[/\\]cpython-[\w.-]+[/\\](bin|Scripts)$/;
|
||||
if (expectedPattern.test(homePath)) {
|
||||
console.log(` ✅ PASS: Placeholder format is correct`);
|
||||
} else {
|
||||
console.log(` ⚠️ WARNING: Placeholder format might be incorrect`);
|
||||
console.log(` Expected: {{PREBUILT_PYTHON_DIR}}/cpython-X.Y.Z-platform/bin (or \\ on Windows)`);
|
||||
console.log(
|
||||
` Expected: {{PREBUILT_PYTHON_DIR}}/cpython-X.Y.Z-platform/bin (or \\ on Windows)`
|
||||
);
|
||||
console.log(` Got: ${homePath}`);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
21
src/App.tsx
21
src/App.tsx
|
|
@ -12,12 +12,10 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import animationData from '@/assets/animation/openning_animaiton.json';
|
||||
import { AnimationJson } from '@/components/AnimationJson';
|
||||
import AppRoutes from '@/routers/index';
|
||||
import { stackClientApp } from '@/stack/client';
|
||||
import { StackProvider, StackTheme } from '@stackframe/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Toaster } from 'sonner';
|
||||
import { hasStackKeys } from './lib';
|
||||
|
|
@ -28,8 +26,6 @@ const HAS_STACK_KEYS = hasStackKeys();
|
|||
function App() {
|
||||
const navigate = useNavigate();
|
||||
const { setInitState } = useAuthStore();
|
||||
const [animationFinished, setAnimationFinished] = useState(false);
|
||||
const { isFirstLaunch } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleShareCode = (event: any, share_token: string) => {
|
||||
|
|
@ -67,19 +63,6 @@ function App() {
|
|||
};
|
||||
}, [navigate, setInitState]);
|
||||
|
||||
// render main content
|
||||
const renderMainContent = () => {
|
||||
if (isFirstLaunch && !animationFinished) {
|
||||
return (
|
||||
<AnimationJson
|
||||
onComplete={() => setAnimationFinished(true)}
|
||||
animationData={animationData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <AppRoutes />;
|
||||
};
|
||||
|
||||
// render wrapper
|
||||
const renderWrapper = (children: React.ReactNode) => {
|
||||
if (HAS_STACK_KEYS) {
|
||||
|
|
@ -98,7 +81,7 @@ function App() {
|
|||
);
|
||||
};
|
||||
|
||||
return renderWrapper(renderMainContent());
|
||||
return renderWrapper(<AppRoutes />);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
BIN
src/assets/background.png
Normal file
BIN
src/assets/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
BIN
src/assets/dmg-background.png
Normal file
BIN
src/assets/dmg-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 586 KiB |
BIN
src/assets/logo/eigent_icon.png
Normal file
BIN
src/assets/logo/eigent_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { fetchPut } from '@/api/http';
|
||||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { TaskStatus } from '@/types/constants';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { TaskState } from '../TaskState';
|
||||
import { Button } from '../ui/button';
|
||||
import { TaskStatus } from "@/types/constants";
|
||||
|
||||
export default function Home() {
|
||||
//Get Chatstore for the active project's task
|
||||
|
|
@ -224,7 +224,8 @@ export default function Home() {
|
|||
}
|
||||
done={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) => task.status === TaskStatus.COMPLETED && !task.reAssignTo
|
||||
(task) =>
|
||||
task.status === TaskStatus.COMPLETED && !task.reAssignTo
|
||||
).length || 0
|
||||
}
|
||||
progress={
|
||||
|
|
@ -240,7 +241,8 @@ export default function Home() {
|
|||
}
|
||||
failed={
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) => task.status === TaskStatus.FAILED && !task.reAssignTo
|
||||
(task) =>
|
||||
task.status === TaskStatus.FAILED && !task.reAssignTo
|
||||
).length || 0
|
||||
}
|
||||
skipped={
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { type ChatTaskStatusType } from '@/types/constants';
|
||||
import { BoxAction } from './BoxAction';
|
||||
import { BoxHeaderConfirm, BoxHeaderSplitting } from './BoxHeader';
|
||||
import { FileAttachment, Inputbox, InputboxProps } from './InputBox';
|
||||
import { QueuedBox, QueuedMessage } from './QueuedBox';
|
||||
import { type ChatTaskStatusType } from "@/types/constants";
|
||||
|
||||
export type BottomBoxState =
|
||||
| 'input'
|
||||
|
|
|
|||
|
|
@ -12,133 +12,133 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { isHtmlDocument } from "@/lib/htmlFontStyles";
|
||||
import { isHtmlDocument } from '@/lib/htmlFontStyles';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
export const SummaryMarkDown = ({
|
||||
content,
|
||||
speed = 15,
|
||||
onTyping,
|
||||
enableTypewriter = true,
|
||||
content,
|
||||
speed = 15,
|
||||
onTyping,
|
||||
enableTypewriter = true,
|
||||
}: {
|
||||
content: string;
|
||||
speed?: number;
|
||||
onTyping?: () => void;
|
||||
enableTypewriter?: boolean;
|
||||
content: string;
|
||||
speed?: number;
|
||||
onTyping?: () => void;
|
||||
enableTypewriter?: boolean;
|
||||
}) => {
|
||||
const [displayedContent, setDisplayedContent] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(true);
|
||||
const [displayedContent, setDisplayedContent] = useState('');
|
||||
const [_isTyping, setIsTyping] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTypewriter) {
|
||||
setDisplayedContent(content);
|
||||
setIsTyping(false);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!enableTypewriter) {
|
||||
setDisplayedContent(content);
|
||||
setIsTyping(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayedContent("");
|
||||
setIsTyping(true);
|
||||
let index = 0;
|
||||
setDisplayedContent('');
|
||||
setIsTyping(true);
|
||||
let index = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (index < content.length) {
|
||||
setDisplayedContent(content.slice(0, index + 1));
|
||||
index++;
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, speed);
|
||||
const timer = setInterval(() => {
|
||||
if (index < content.length) {
|
||||
setDisplayedContent(content.slice(0, index + 1));
|
||||
index++;
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, speed);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [content, speed, onTyping]);
|
||||
return () => clearInterval(timer);
|
||||
}, [content, speed, onTyping, enableTypewriter]);
|
||||
|
||||
// If content is a pure HTML document, render in a styled pre block
|
||||
if (isHtmlDocument(content)) {
|
||||
// Trim leading whitespace from each line for consistent alignment
|
||||
const formattedHtml = displayedContent
|
||||
.split('\n')
|
||||
.map(line => line.trimStart())
|
||||
.join('\n')
|
||||
.trim();
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<pre className="bg-emerald-50 border border-emerald-200 p-3 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap mb-3">
|
||||
<code>{formattedHtml}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If content is a pure HTML document, render in a styled pre block
|
||||
if (isHtmlDocument(content)) {
|
||||
// Trim leading whitespace from each line for consistent alignment
|
||||
const formattedHtml = displayedContent
|
||||
.split('\n')
|
||||
.map((line) => line.trimStart())
|
||||
.join('\n')
|
||||
.trim();
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 font-mono text-xs">
|
||||
<code>{formattedHtml}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-xl font-bold text-emerald-800 mb-3 flex items-center gap-2 border-b border-emerald-200 pb-2">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-lg font-semibold text-emerald-700 mb-3 mt-4 flex items-center gap-2">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-base font-medium text-emerald-600 mb-2 mt-3">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="m-0 text-sm font-normal text-gray-700 leading-relaxed mb-3 whitespace-pre-wrap">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-sm text-gray-700 mb-3 space-y-1 ml-2">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-sm text-gray-700 mb-3 space-y-1 ml-2">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 text-gray-700 leading-relaxed">{children}</li>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="bg-surface-success-subtle text-text-success px-2 py-1 rounded text-xs font-mono">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-emerald-50 border border-emerald-200 p-3 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap mb-3">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-emerald-300 pl-4 italic text-emerald-700 bg-emerald-50 py-2 rounded-r-lg mb-3">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-emerald-800">{children}</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic text-emerald-600">{children}</em>
|
||||
),
|
||||
hr: () => (
|
||||
<hr className="border-emerald-200 my-4" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{displayedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="mb-3 flex items-center gap-2 border-b border-emerald-200 pb-2 text-xl font-bold text-emerald-800">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="mb-3 mt-4 flex items-center gap-2 text-lg font-semibold text-emerald-700">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="mb-2 mt-3 text-base font-medium text-emerald-600">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="m-0 mb-3 whitespace-pre-wrap text-sm font-normal leading-relaxed text-gray-700">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="mb-3 ml-2 list-inside list-disc space-y-1 text-sm text-gray-700">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="mb-3 ml-2 list-inside list-decimal space-y-1 text-sm text-gray-700">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 leading-relaxed text-gray-700">{children}</li>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="rounded bg-surface-success-subtle px-2 py-1 font-mono text-xs text-text-success">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 font-mono text-xs">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="mb-3 rounded-r-lg border-l-4 border-emerald-300 bg-emerald-50 py-2 pl-4 italic text-emerald-700">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-emerald-800">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic text-emerald-600">{children}</em>
|
||||
),
|
||||
hr: () => <hr className="my-4 border-emerald-200" />,
|
||||
}}
|
||||
>
|
||||
{displayedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { VanillaChatStore } from '@/store/chatStore';
|
||||
import { AgentStep } from '@/types/constants';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
import { FloatingAction } from './FloatingAction';
|
||||
import { UserQueryGroup } from './UserQueryGroup';
|
||||
import { AgentStep } from '@/types/constants';
|
||||
|
||||
interface ProjectSectionProps {
|
||||
chatId: string;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
|
|||
size={16}
|
||||
className="animate-spin text-icon-information"
|
||||
/>
|
||||
<span className="text-text-secondary animate-pulse text-sm">
|
||||
<span className="animate-pulse text-sm text-text-secondary">
|
||||
{t('layout.task-splitting')}...
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -101,7 +101,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
|
|||
{/* Task type badge */}
|
||||
<div className="mb-2 flex items-center gap-2 px-sm">
|
||||
<TaskType type={1} />
|
||||
<span className="text-text-tertiary text-xs font-medium">
|
||||
<span className="text-xs font-medium text-text-tertiary">
|
||||
{t('layout.tasks')} {tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -131,7 +131,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
|
|||
|
||||
{/* Task content */}
|
||||
<div className="relative flex min-h-4 w-full items-start border-[0px] border-b border-solid border-task-border-default pb-2">
|
||||
<span className="text-text-primary text-xs leading-[20px]">
|
||||
<span className="text-xs leading-[20px] text-text-primary">
|
||||
{task}
|
||||
{isCurrentlyStreaming && (
|
||||
<span className="ml-0.5 inline-block h-4 w-1 animate-pulse bg-icon-information" />
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const TypeCardSkeleton = ({
|
|||
|
||||
<div className="transition-all duration-300 ease-in-out">
|
||||
<div className="flex items-center gap-2 duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<div className="text-text-tertiary text-xs font-medium leading-17">
|
||||
<div className="text-xs font-medium leading-17 text-text-tertiary">
|
||||
{t('layout.tasks')}
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { VanillaChatStore } from '@/store/chatStore';
|
||||
import { AgentStep, ChatTaskStatus } from '@/types/constants';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FileText } from 'lucide-react';
|
||||
import React, {
|
||||
|
|
@ -27,7 +28,6 @@ import { UserMessageCard } from './MessageItem/UserMessageCard';
|
|||
import { StreamingTaskList } from './TaskBox/StreamingTaskList';
|
||||
import { TaskCard } from './TaskBox/TaskCard';
|
||||
import { TypeCardSkeleton } from './TaskBox/TypeCardSkeleton';
|
||||
import { AgentStep, ChatTaskStatus } from '@/types/constants';
|
||||
|
||||
interface QueryGroup {
|
||||
queryId: string;
|
||||
|
|
@ -87,7 +87,9 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
|
|||
if (userMessageIndex > 0) {
|
||||
// Check the previous message - if it's an agent message with step 'ask', this is a human-reply
|
||||
const prevMessage = messages[userMessageIndex - 1];
|
||||
return prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK;
|
||||
return (
|
||||
prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})());
|
||||
|
|
|
|||
|
|
@ -12,50 +12,65 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger?: React.ReactNode;
|
||||
onConfirm: () => void;
|
||||
loading?: boolean;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger?: React.ReactNode;
|
||||
onConfirm: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function EndNoticeDialog({ open, onOpenChange, trigger, onConfirm, loading = false }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const onSubmit = useCallback(() => {
|
||||
onConfirm();
|
||||
}, [onConfirm]);
|
||||
export default function EndNoticeDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
trigger,
|
||||
onConfirm,
|
||||
loading = false,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const onSubmit = useCallback(() => {
|
||||
onConfirm();
|
||||
}, [onConfirm]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent className="sm:max-w-[600px] p-0 !bg-popup-surface gap-0 !rounded-xl border border-border-subtle-strong shadow-sm">
|
||||
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md justify-start">
|
||||
<DialogTitle className="m-0">{t("layout.end-project")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-md bg-popup-bg p-md">
|
||||
{t("layout.ending-this-project-will-stop")}
|
||||
</div>
|
||||
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost" size="md" disabled={loading}>{t("layout.cancel")}</Button>
|
||||
</DialogClose>
|
||||
<Button size="md" onClick={onSubmit} variant="cuation" disabled={loading}>{t("layout.yes-end-project")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent className="gap-0 !rounded-xl border border-border-subtle-strong !bg-popup-surface p-0 shadow-sm sm:max-w-[600px]">
|
||||
<DialogHeader className="justify-start !rounded-t-xl !bg-popup-surface p-md">
|
||||
<DialogTitle className="m-0">{t('layout.end-project')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-md bg-popup-bg p-md">
|
||||
{t('layout.ending-this-project-will-stop')}
|
||||
</div>
|
||||
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost" size="md" disabled={loading}>
|
||||
{t('layout.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
size="md"
|
||||
onClick={onSubmit}
|
||||
variant="cuation"
|
||||
disabled={loading}
|
||||
>
|
||||
{t('layout.yes-end-project')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,186 +12,190 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { proxyFetchGet, proxyFetchPut } from '@/api/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { proxyFetchGet, proxyFetchPut } from "@/api/http";
|
||||
import { useTranslation } from "react-i18next";
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface PrivacyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger?: React.ReactNode;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PrivacyDialog({ open, onOpenChange, trigger }: PrivacyDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const API_FIELDS = [
|
||||
"take_screenshot",
|
||||
"access_local_software",
|
||||
"access_your_address",
|
||||
"password_storage",
|
||||
];
|
||||
export function PrivacyDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
trigger,
|
||||
}: PrivacyDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const API_FIELDS = useMemo(
|
||||
() => [
|
||||
'take_screenshot',
|
||||
'access_local_software',
|
||||
'access_your_address',
|
||||
'password_storage',
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const [settings, setSettings] = useState([
|
||||
{
|
||||
title: t("layout.allow-agent-to-take-screenshots"),
|
||||
description: t("layout.permit-the-agent-to-capture"),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t("layout.allow-agent-to-access-local-software"),
|
||||
description: t("layout.grant-the-agent-permission"),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t("layout.allow-agent-to-access-your-address"),
|
||||
description: t("layout.authorize-the-agent-to-view"),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t("layout.password-storage"),
|
||||
description: t("layout.determine-how-passwords-are-handled"),
|
||||
checked: false,
|
||||
},
|
||||
]);
|
||||
const [settings, setSettings] = useState([
|
||||
{
|
||||
title: t('layout.allow-agent-to-take-screenshots'),
|
||||
description: t('layout.permit-the-agent-to-capture'),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t('layout.allow-agent-to-access-local-software'),
|
||||
description: t('layout.grant-the-agent-permission'),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t('layout.allow-agent-to-access-your-address'),
|
||||
description: t('layout.authorize-the-agent-to-view'),
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
title: t('layout.password-storage'),
|
||||
description: t('layout.determine-how-passwords-are-handled'),
|
||||
checked: false,
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
proxyFetchGet("/api/user/privacy")
|
||||
.then((res) => {
|
||||
setSettings((prev) =>
|
||||
prev.map((item, index) => ({
|
||||
...item,
|
||||
checked: res[API_FIELDS[index]] || false,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err) => console.error("Failed to fetch settings:", err));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/user/privacy')
|
||||
.then((res) => {
|
||||
setSettings((prev) =>
|
||||
prev.map((item, index) => ({
|
||||
...item,
|
||||
checked: res[API_FIELDS[index]] || false,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err) => console.error('Failed to fetch settings:', err));
|
||||
}, [API_FIELDS]);
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
setSettings((prev) => {
|
||||
const newSettings = [...prev];
|
||||
newSettings[index] = {
|
||||
...newSettings[index],
|
||||
checked: !newSettings[index].checked,
|
||||
};
|
||||
return newSettings;
|
||||
});
|
||||
const handleToggle = (index: number) => {
|
||||
setSettings((prev) => {
|
||||
const newSettings = [...prev];
|
||||
newSettings[index] = {
|
||||
...newSettings[index],
|
||||
checked: !newSettings[index].checked,
|
||||
};
|
||||
return newSettings;
|
||||
});
|
||||
|
||||
const requestData = {
|
||||
[API_FIELDS[0]]: settings[0].checked,
|
||||
[API_FIELDS[1]]: settings[1].checked,
|
||||
[API_FIELDS[2]]: settings[2].checked,
|
||||
[API_FIELDS[3]]: settings[3].checked,
|
||||
};
|
||||
const requestData = {
|
||||
[API_FIELDS[0]]: settings[0].checked,
|
||||
[API_FIELDS[1]]: settings[1].checked,
|
||||
[API_FIELDS[2]]: settings[2].checked,
|
||||
[API_FIELDS[3]]: settings[3].checked,
|
||||
};
|
||||
|
||||
requestData[API_FIELDS[index]] = !settings[index].checked;
|
||||
requestData[API_FIELDS[index]] = !settings[index].checked;
|
||||
|
||||
proxyFetchPut("/api/user/privacy", requestData).catch((err) =>
|
||||
console.error("Failed to update settings:", err)
|
||||
);
|
||||
};
|
||||
proxyFetchPut('/api/user/privacy', requestData).catch((err) =>
|
||||
console.error('Failed to update settings:', err)
|
||||
);
|
||||
};
|
||||
|
||||
const handleTurnOnAll = () => {
|
||||
const newSettings = settings.map((item) => ({
|
||||
...item,
|
||||
checked: true,
|
||||
}));
|
||||
setSettings(newSettings);
|
||||
const handleTurnOnAll = () => {
|
||||
const newSettings = settings.map((item) => ({
|
||||
...item,
|
||||
checked: true,
|
||||
}));
|
||||
setSettings(newSettings);
|
||||
|
||||
const requestData = {
|
||||
[API_FIELDS[0]]: true,
|
||||
[API_FIELDS[1]]: true,
|
||||
[API_FIELDS[2]]: true,
|
||||
[API_FIELDS[3]]: true,
|
||||
};
|
||||
const requestData = {
|
||||
[API_FIELDS[0]]: true,
|
||||
[API_FIELDS[1]]: true,
|
||||
[API_FIELDS[2]]: true,
|
||||
[API_FIELDS[3]]: true,
|
||||
};
|
||||
|
||||
proxyFetchPut("/api/user/privacy", requestData)
|
||||
.then(() => {
|
||||
onOpenChange(false);
|
||||
})
|
||||
.catch((err) => console.error("Failed to update settings:", err));
|
||||
};
|
||||
proxyFetchPut('/api/user/privacy', requestData)
|
||||
.then(() => {
|
||||
onOpenChange(false);
|
||||
})
|
||||
.catch((err) => console.error('Failed to update settings:', err));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent className="sm:max-w-[600px] p-0 !bg-popup-surface gap-0 !rounded-xl border border-border-subtle-strong shadow-sm">
|
||||
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md">
|
||||
<DialogTitle className="m-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-base font-bold leading-10 text-text-action">
|
||||
{t("layout.turn-on-all-privacy-settings")}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertCircle
|
||||
size={16}
|
||||
className="text-icon-primary cursor-pointer"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[340px]">
|
||||
<p className="text-text-body text-sm">{t("layout.eigent-is-a-desktop-software")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent className="gap-0 !rounded-xl border border-border-subtle-strong !bg-popup-surface p-0 shadow-sm sm:max-w-[600px]">
|
||||
<DialogHeader className="!rounded-t-xl !bg-popup-surface p-md">
|
||||
<DialogTitle className="m-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-base font-bold leading-10 text-text-action">
|
||||
{t('layout.turn-on-all-privacy-settings')}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertCircle
|
||||
size={16}
|
||||
className="cursor-pointer text-icon-primary"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[340px]">
|
||||
<p className="text-sm text-text-body">
|
||||
{t('layout.eigent-is-a-desktop-software')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-md bg-popup-bg p-md">
|
||||
{settings.map((item, index) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="flex gap-md items-start mb-4"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-bold text-text-primary mb-1">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="text-xs text-text-body">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Switch
|
||||
checked={item.checked}
|
||||
onCheckedChange={() => handleToggle(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-md bg-popup-bg p-md">
|
||||
{settings.map((item, index) => (
|
||||
<div key={item.title} className="mb-4 flex items-start gap-md">
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 text-sm font-bold text-text-primary">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="text-xs text-text-body">{item.description}</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Switch
|
||||
checked={item.checked}
|
||||
onCheckedChange={() => handleToggle(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost" size="md">
|
||||
{t("layout.cancel")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button size="md" onClick={handleTurnOnAll} variant="primary">
|
||||
{t("layout.turn-on-all-and-finish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost" size="md">
|
||||
{t('layout.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button size="md" onClick={handleTurnOnAll} variant="primary">
|
||||
{t('layout.turn-on-all-and-finish')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,6 +390,9 @@ export default function Folder({ data: _data }: { data?: Agent }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning]);
|
||||
|
||||
const selectedFilePath =
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.selectedFile?.path;
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatStore) return;
|
||||
const chatStoreSelectedFile =
|
||||
|
|
@ -403,12 +406,7 @@ export default function Folder({ data: _data }: { data?: Agent }) {
|
|||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.selectedFile?.path,
|
||||
fileGroups,
|
||||
isShowSourceCode,
|
||||
chatStore?.activeTaskId,
|
||||
]);
|
||||
}, [selectedFilePath, fileGroups, isShowSourceCode, chatStore?.activeTaskId]);
|
||||
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ export function GlobalSearch() {
|
|||
className="bg-bg-surface-secondary no-drag flex h-6 w-60 items-center justify-center space-x-2 rounded-lg"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Search className="text-text-secondary h-4 w-4"></Search>
|
||||
<span className="text-text-secondary font-inter text-[10px] leading-4">
|
||||
<Search className="h-4 w-4 text-text-secondary"></Search>
|
||||
<span className="font-inter text-[10px] leading-4 text-text-secondary">
|
||||
{t('dashboard.search-for-a-task-or-document')}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import { TooltipSimple } from '@/components/ui/tooltip';
|
|||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { replayProject } from '@/lib/replay';
|
||||
import { useProjectStore } from '@/store/projectStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { ProjectGroup as ProjectGroupType } from '@/types/history';
|
||||
import { ChatTaskStatus } from "@/types/constants";
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Edit,
|
||||
|
|
@ -101,7 +101,8 @@ export default function ProjectGroup({
|
|||
// Check if any task in chatStore with matching task_id has pending status
|
||||
return Object.entries(chatStore.tasks).some(
|
||||
([taskId, task]) =>
|
||||
projectTaskIds.includes(taskId) && task.status === ChatTaskStatus.PENDING
|
||||
projectTaskIds.includes(taskId) &&
|
||||
task.status === ChatTaskStatus.PENDING
|
||||
);
|
||||
}, [chatStore?.tasks, project.tasks]);
|
||||
const _hasIssue = hasHumanInLoop;
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import {
|
|||
} from '@/components/ui/popover';
|
||||
import { Tag } from '@/components/ui/tag';
|
||||
import { TooltipSimple } from '@/components/ui/tooltip';
|
||||
import { HistoryTask } from '@/types/history';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { HistoryTask } from '@/types/history';
|
||||
import {
|
||||
CheckCircle,
|
||||
CirclePause,
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export default function GroupedHistoryView({
|
|||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-icon-secondary" />
|
||||
<span className="text-text-secondary ml-2">{t('layout.loading')}</span>
|
||||
<span className="ml-2 text-text-secondary">{t('layout.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -302,13 +302,13 @@ export default function GroupedHistoryView({
|
|||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||
<FolderOpen className="text-icon-tertiary mb-4 h-12 w-12" />
|
||||
<div className="text-text-secondary text-sm">
|
||||
<div className="text-sm text-text-secondary">
|
||||
{searchValue
|
||||
? t('dashboard.no-projects-match-search')
|
||||
: t('dashboard.no-projects-found')}
|
||||
</div>
|
||||
{searchValue && (
|
||||
<div className="text-text-tertiary mt-1 text-xs">
|
||||
<div className="mt-1 text-xs text-text-tertiary">
|
||||
{t('dashboard.try-different-search')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { share } from '@/lib/share';
|
|||
import { fetchGroupedHistoryTasks } from '@/service/historyApi';
|
||||
import { getAuthStore } from '@/store/authStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { HistoryTask, ProjectGroup } from '@/types/history';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
|
|
@ -45,7 +46,6 @@ import {
|
|||
import { Tag } from '../ui/tag';
|
||||
import { TooltipSimple } from '../ui/tooltip';
|
||||
import SearchInput from './SearchInput';
|
||||
import { ChatTaskStatus } from "@/types/constants";
|
||||
|
||||
export default function HistorySidebar() {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import addWorkerVideo from '@/assets/add_worker.mp4';
|
||||
import dynamicWorkforceVideo from '@/assets/dynamic_workforce.mp4';
|
||||
import localModelVideo from '@/assets/local_model.mp4';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Carousel,
|
||||
|
|
@ -19,12 +23,9 @@ import {
|
|||
CarouselItem,
|
||||
} from '@/components/ui/carousel';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { Pause, Play } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import addWorkerVideo from '@/assets/add_worker.mp4';
|
||||
import dynamicWorkforceVideo from '@/assets/dynamic_workforce.mp4';
|
||||
import localModelVideo from '@/assets/local_model.mp4';
|
||||
|
||||
export const CarouselStep: React.FC = () => {
|
||||
const { setInitState: _setInitState } = useAuthStore();
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
|
@ -32,11 +33,19 @@ export const CarouselStep: React.FC = () => {
|
|||
const [api, setApi] = useState<any>(null);
|
||||
const [isDismissed, _setIsDismissed] = useState(false);
|
||||
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const videoEndTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// listen to carousel change
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
|
||||
const onSelect = () => {
|
||||
// Clear any pending video end timeout when slide changes manually
|
||||
if (videoEndTimeoutRef.current) {
|
||||
clearTimeout(videoEndTimeoutRef.current);
|
||||
videoEndTimeoutRef.current = null;
|
||||
}
|
||||
setCurrentSlide(api.selectedScrollSnap());
|
||||
};
|
||||
|
||||
|
|
@ -46,6 +55,15 @@ export const CarouselStep: React.FC = () => {
|
|||
};
|
||||
}, [api]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (videoEndTimeoutRef.current) {
|
||||
clearTimeout(videoEndTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// click indicator to jump to corresponding slide
|
||||
const scrollTo = (index: number) => {
|
||||
if (api) {
|
||||
|
|
@ -65,17 +83,34 @@ export const CarouselStep: React.FC = () => {
|
|||
const handleIndicatorHover = (index: number) => {
|
||||
scrollTo(index);
|
||||
};
|
||||
|
||||
const handleTogglePause = () => {
|
||||
const newPausedState = !isPaused;
|
||||
setIsPaused(newPausedState);
|
||||
|
||||
const currentVideo = videoRefs.current[currentSlide];
|
||||
if (currentVideo) {
|
||||
if (newPausedState) {
|
||||
currentVideo.pause();
|
||||
} else {
|
||||
currentVideo.play().catch((err) => {
|
||||
console.warn('video.play() error:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const carouselItems = [
|
||||
{
|
||||
title: '“Dynamic Workforce break it down, get task done”',
|
||||
title: 'Dynamic Workforce break it down, get task done',
|
||||
video: dynamicWorkforceVideo,
|
||||
},
|
||||
{
|
||||
title: '“Add worker with pluggable mcp”',
|
||||
title: 'Add worker with pluggable MCP',
|
||||
video: addWorkerVideo,
|
||||
},
|
||||
{
|
||||
title: '“private and secure with local model settings”',
|
||||
title: 'Private and secure with local model settings',
|
||||
video: localModelVideo,
|
||||
},
|
||||
];
|
||||
|
|
@ -87,9 +122,11 @@ export const CarouselStep: React.FC = () => {
|
|||
if (video) {
|
||||
const tryPlay = () => {
|
||||
video.currentTime = 0;
|
||||
video.play().catch((err) => {
|
||||
console.warn('video.play() error:', err);
|
||||
});
|
||||
if (!isPaused) {
|
||||
video.play().catch((err) => {
|
||||
console.warn('video.play() error:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (video.readyState >= 1) {
|
||||
|
|
@ -104,7 +141,7 @@ export const CarouselStep: React.FC = () => {
|
|||
video.addEventListener('loadedmetadata', handler);
|
||||
}
|
||||
}
|
||||
}, [currentSlide, api]);
|
||||
}, [currentSlide, api, isPaused]);
|
||||
|
||||
// If carousel is dismissed, don't show anything
|
||||
// The actual transition to 'done' will be handled by useInstallationSetup
|
||||
|
|
@ -114,14 +151,14 @@ export const CarouselStep: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-[1120px] flex-col gap-lg max-lg:w-[100%]">
|
||||
<div className="flex flex-col gap-md">
|
||||
<div className="text-4xl font-bold leading-5xl text-text-heading">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-full min-h-0 w-full flex-col">
|
||||
<div className="mb-md text-heading-sm font-bold text-text-heading">
|
||||
{carouselItems[currentSlide].title}
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
className="scrollbar max-h-[490px] min-h-[400px] rounded-3xl bg-white-100% p-0 short:max-h-[300px] short:overflow-y-auto"
|
||||
className="min-h-0 flex-1 bg-transparent"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
setApi={setApi}
|
||||
|
|
@ -129,26 +166,45 @@ export const CarouselStep: React.FC = () => {
|
|||
<CarouselContent className="h-full">
|
||||
{carouselItems.map((_, index) => (
|
||||
<CarouselItem key={index} className="h-full">
|
||||
<div className="h-full p-0">
|
||||
<CardContent className="h-full w-full items-center justify-center p-0">
|
||||
<video
|
||||
ref={(el) => (videoRefs.current[index] = el)}
|
||||
src={carouselItems[index].video}
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
onEnded={() => {
|
||||
if (api) {
|
||||
const currentIndex = api.selectedScrollSnap();
|
||||
if (currentIndex < carouselItems.length - 1) {
|
||||
api.scrollNext();
|
||||
} else {
|
||||
api.scrollTo(0);
|
||||
<div className="h-full w-full p-0">
|
||||
<CardContent className="flex h-full w-full items-center justify-center p-0">
|
||||
<div
|
||||
key={
|
||||
index === currentSlide
|
||||
? `slide-active-${currentSlide}`
|
||||
: `slide-${index}`
|
||||
}
|
||||
className={`h-full w-full ${
|
||||
index === currentSlide ? 'animate-fade-in' : ''
|
||||
}`}
|
||||
>
|
||||
<video
|
||||
ref={(el) => (videoRefs.current[index] = el)}
|
||||
src={carouselItems[index].video}
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
onEnded={() => {
|
||||
if (api && !isPaused) {
|
||||
// Clear any existing timeout
|
||||
if (videoEndTimeoutRef.current) {
|
||||
clearTimeout(videoEndTimeoutRef.current);
|
||||
}
|
||||
// Wait 2 seconds before moving to next video
|
||||
videoEndTimeoutRef.current = setTimeout(() => {
|
||||
const currentIndex = api.selectedScrollSnap();
|
||||
if (currentIndex < carouselItems.length - 1) {
|
||||
api.scrollNext();
|
||||
} else {
|
||||
api.scrollTo(0);
|
||||
}
|
||||
videoEndTimeoutRef.current = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-full w-full rounded-3xl object-contain"
|
||||
/>
|
||||
}}
|
||||
className="h-full w-full rounded-3xl object-contain"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
|
|
@ -156,20 +212,33 @@ export const CarouselStep: React.FC = () => {
|
|||
</CarouselContent>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-sm">
|
||||
<div className="relative mt-2 flex items-center justify-center gap-sm">
|
||||
<div className="flex items-center justify-center gap-6">
|
||||
{carouselItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onMouseEnter={() => handleIndicatorHover(index)}
|
||||
className={`h-1.5 w-[120px] cursor-pointer rounded-full transition-all duration-300 ${
|
||||
className={`h-1 w-32 cursor-pointer rounded-full transition-all duration-300 ${
|
||||
index === currentSlide
|
||||
? 'bg-fill-fill-secondary'
|
||||
: 'bg-white-100% hover:bg-fill-fill-secondary'
|
||||
: 'bg-fill-fill-tertiary hover:bg-fill-fill-secondary'
|
||||
}`}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleTogglePause}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute bottom-0 right-0 rounded-full"
|
||||
aria-label={isPaused ? 'Resume' : 'Pause'}
|
||||
>
|
||||
{isPaused ? (
|
||||
<Play className="h-4 w-4" />
|
||||
) : (
|
||||
<Pause className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,32 +26,44 @@ export const InstallDependencies: React.FC = () => {
|
|||
useInstallationUI();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 !z-[100] flex h-full w-full items-center justify-center bg-opacity-80 backdrop-blur-sm">
|
||||
<div className="flex h-full w-[1200px] flex-col justify-center gap-xl p-[40px]">
|
||||
<div className="relative">
|
||||
<div className="fixed inset-0 !z-[100] flex h-full w-full items-center justify-center overflow-hidden px-2 pb-2 pt-10">
|
||||
<div className="flex h-full w-full flex-row justify-center gap-lg rounded-2xl border-solid border-border-tertiary bg-surface-secondary p-md">
|
||||
<div className="flex h-full w-1/3 pt-6">
|
||||
{/* {isInstalling.toString()} */}
|
||||
<div>
|
||||
<div className="flex w-full flex-col">
|
||||
<ProgressInstall
|
||||
value={
|
||||
isInstalling || installationState === 'waiting-backend'
|
||||
? progress
|
||||
: 100
|
||||
}
|
||||
className="w-full"
|
||||
className="mb-4 w-full"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-xs font-normal leading-tight text-text-label">
|
||||
{isInstalling
|
||||
? 'System Installing ...'
|
||||
: installationState === 'waiting-backend'
|
||||
? 'Starting backend service...'
|
||||
: ''}
|
||||
<span className="pl-2">{latestLog?.data}</span>
|
||||
<div className="mt-2 flex w-full flex-col items-start justify-between gap-4">
|
||||
<div className="flex w-full flex-row items-start justify-between">
|
||||
<div className="text-body-sm font-medium leading-normal text-text-heading">
|
||||
{isInstalling
|
||||
? 'System Installing ...'
|
||||
: installationState === 'waiting-backend'
|
||||
? 'Starting backend service...'
|
||||
: ''}
|
||||
</div>
|
||||
<div className="text-body-sm font-medium leading-normal text-text-heading">
|
||||
{Math.round(
|
||||
(isInstalling || installationState === 'waiting-backend'
|
||||
? progress
|
||||
: 100) ?? 0
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full text-body-sm font-normal leading-normal text-text-label">
|
||||
{latestLog?.data}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex h-full w-2/3 rounded-2xl bg-surface-tertiary p-md">
|
||||
{initState === 'permissions' && <Permissions />}
|
||||
{initState === 'carousel' &&
|
||||
installationState !== 'waiting-backend' && <CarouselStep />}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogContentSection,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { t } from 'i18next';
|
||||
|
||||
|
|
@ -42,18 +41,20 @@ const InstallationErrorDialog = ({
|
|||
if (backendError) {
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('layout.backend-startup-failed')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 text-xs font-normal leading-tight text-text-label">
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label/60">{backendError}</span>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader title={t('layout.backend-startup-failed')} />
|
||||
<DialogContentSection>
|
||||
<div className="text-xs font-normal leading-normal text-text-label">
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label">{backendError}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={retryBackend}>{t('layout.retry')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText={t('layout.retry')}
|
||||
onConfirm={retryBackend}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
@ -61,18 +62,20 @@ const InstallationErrorDialog = ({
|
|||
|
||||
return (
|
||||
<Dialog open={installationState == 'error'}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('layout.installation-failed')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mb-4 text-xs font-normal leading-tight text-text-label">
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label/60">{error}</span>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader title={t('layout.installation-failed')} />
|
||||
<DialogContentSection>
|
||||
<div className="text-xs font-normal leading-normal text-text-label">
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={retryInstallation}>{t('layout.retry')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContentSection>
|
||||
<DialogFooter
|
||||
showConfirmButton
|
||||
confirmButtonText={t('layout.retry')}
|
||||
onConfirm={retryInstallation}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -84,13 +84,13 @@ export const Permissions: React.FC = () => {
|
|||
);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-lg">
|
||||
<div className="flex h-[568px] gap-md">
|
||||
<div className="flex w-[438px] flex-col gap-md">
|
||||
<div className="text-4xl font-bold leading-5xl text-text-heading">
|
||||
<div>Enable Permissions</div>
|
||||
<div className="flex h-full w-full flex-col gap-lg">
|
||||
<div className="flex h-full w-full gap-md">
|
||||
<div className="flex w-full flex-col gap-md">
|
||||
<div className="text-heading-sm font-bold text-text-heading">
|
||||
Enable Permissions
|
||||
</div>
|
||||
<div className="text-xl font-medium leading-2xl text-text-body">
|
||||
<div className="text-body-md font-medium text-text-body">
|
||||
${`Grant permission to activate the Agent's autonomous actions.`}
|
||||
</div>
|
||||
{settings.map((item, index) => (
|
||||
|
|
@ -120,8 +120,8 @@ export const Permissions: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-sm">
|
||||
<div className="flex items-center justify-center gap-sm">
|
||||
<div className="flex h-full w-full items-center justify-end gap-sm">
|
||||
<div className="flex w-full items-center justify-center gap-sm">
|
||||
<Button
|
||||
onClick={() => setInitState('carousel')}
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -12,374 +12,376 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Terminal Component Properties Interface
|
||||
interface TerminalComponentProps {
|
||||
content?: string[]; // terminal content array
|
||||
instanceId?: string; // terminal instance identifier, for multiple terminals
|
||||
showWelcome?: boolean; // whether to show welcome information
|
||||
content?: string[]; // terminal content array
|
||||
instanceId?: string; // terminal instance identifier, for multiple terminals
|
||||
showWelcome?: boolean; // whether to show welcome information
|
||||
}
|
||||
|
||||
export default function TerminalComponent({
|
||||
content,
|
||||
instanceId = "default",
|
||||
showWelcome = false,
|
||||
content,
|
||||
instanceId = 'default',
|
||||
showWelcome = false,
|
||||
}: TerminalComponentProps) {
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore } = useChatStoreAdapter();
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore } = useChatStoreAdapter();
|
||||
|
||||
// DOM references
|
||||
const terminalContainerRef = useRef<HTMLDivElement>(null); // terminal container reference
|
||||
const terminalRef = useRef<HTMLDivElement>(null); // terminal element reference
|
||||
|
||||
// DOM references
|
||||
const terminalContainerRef = useRef<HTMLDivElement>(null); // terminal container reference
|
||||
const terminalRef = useRef<HTMLDivElement>(null); // terminal element reference
|
||||
// xterm.js related references
|
||||
const xtermRef = useRef<Terminal | null>(null); // xterm instance reference
|
||||
const fitAddonRef = useRef<FitAddon | null>(null); // fit addon reference
|
||||
|
||||
// xterm.js related references
|
||||
const xtermRef = useRef<Terminal | null>(null); // xterm instance reference
|
||||
const fitAddonRef = useRef<FitAddon | null>(null); // fit addon reference
|
||||
// state management
|
||||
const lastTerminalLength = useRef<number>(0); // record last content length, for incremental update
|
||||
const [currentLine, setCurrentLine] = useState<string>(''); // current input line
|
||||
const [cursorPos, setCursorPos] = useState<number>(0); // cursor position
|
||||
const currentLineRef = useRef<string>(''); // current input line ref, for event handling
|
||||
const cursorPosRef = useRef<number>(0); // cursor position ref, for event handling
|
||||
|
||||
// state management
|
||||
const lastTerminalLength = useRef<number>(0); // record last content length, for incremental update
|
||||
const [currentLine, setCurrentLine] = useState<string>(""); // current input line
|
||||
const [cursorPos, setCursorPos] = useState<number>(0); // cursor position
|
||||
const currentLineRef = useRef<string>(""); // current input line ref, for event handling
|
||||
const cursorPosRef = useRef<number>(0); // cursor position ref, for event handling
|
||||
// terminal configuration
|
||||
const promptText = 'Eigent:~$ '; // prompt text
|
||||
const isInitialized = useRef<boolean>(false); // initialization identifier, prevent duplicate initialization
|
||||
|
||||
// terminal configuration
|
||||
const promptText = "Eigent:~$ "; // prompt text
|
||||
const isInitialized = useRef<boolean>(false); // initialization identifier, prevent duplicate initialization
|
||||
// synchronize state to ref, for event handling
|
||||
useEffect(() => {
|
||||
currentLineRef.current = currentLine;
|
||||
}, [currentLine]);
|
||||
|
||||
// synchronize state to ref, for event handling
|
||||
useEffect(() => {
|
||||
currentLineRef.current = currentLine;
|
||||
}, [currentLine]);
|
||||
useEffect(() => {
|
||||
cursorPosRef.current = cursorPos;
|
||||
}, [cursorPos]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorPosRef.current = cursorPos;
|
||||
}, [cursorPos]);
|
||||
// keyboard input handling function
|
||||
const handleKeyInput = useCallback(
|
||||
({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
|
||||
const ev = domEvent;
|
||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; // check if it is printable character
|
||||
const terminal = xtermRef.current;
|
||||
if (!terminal) return;
|
||||
|
||||
// keyboard input handling function
|
||||
const handleKeyInput = useCallback(
|
||||
({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
|
||||
const ev = domEvent;
|
||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; // check if it is printable character
|
||||
const terminal = xtermRef.current;
|
||||
if (!terminal) return;
|
||||
if (ev.keyCode === 13) {
|
||||
// Enter key: execute command
|
||||
terminal.writeln('');
|
||||
if (currentLineRef.current.trim()) {
|
||||
terminal.writeln(
|
||||
`\x1b[90m# Executed: ${currentLineRef.current}\x1b[0m`
|
||||
);
|
||||
terminal.writeln(
|
||||
`\x1b[33m⚠ Interactive mode not fully implemented\x1b[0m`
|
||||
);
|
||||
}
|
||||
setCurrentLine('');
|
||||
setCursorPos(0);
|
||||
terminal.write(promptText);
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Backspace key: delete character
|
||||
if (cursorPosRef.current > 0) {
|
||||
const newLine =
|
||||
currentLineRef.current.slice(0, cursorPosRef.current - 1) +
|
||||
currentLineRef.current.slice(cursorPosRef.current);
|
||||
setCurrentLine(newLine);
|
||||
setCursorPos(cursorPosRef.current - 1);
|
||||
terminal.write('\b \b'); // delete character before cursor
|
||||
}
|
||||
} else if (ev.keyCode === 37) {
|
||||
// left arrow: move cursor left
|
||||
if (cursorPosRef.current > 0) {
|
||||
setCursorPos(cursorPosRef.current - 1);
|
||||
terminal.write('\x1b[D'); // ANSI escape sequence: move cursor left
|
||||
}
|
||||
} else if (ev.keyCode === 39) {
|
||||
// right arrow: move cursor right
|
||||
if (cursorPosRef.current < currentLineRef.current.length) {
|
||||
setCursorPos(cursorPosRef.current + 1);
|
||||
terminal.write('\x1b[C'); // ANSI escape sequence: move cursor right
|
||||
}
|
||||
} else if (printable) {
|
||||
// printable character: insert at cursor position
|
||||
const newLine =
|
||||
currentLineRef.current.slice(0, cursorPosRef.current) +
|
||||
key +
|
||||
currentLineRef.current.slice(cursorPosRef.current);
|
||||
setCurrentLine(newLine);
|
||||
setCursorPos(cursorPosRef.current + 1);
|
||||
terminal.write(key);
|
||||
}
|
||||
},
|
||||
[promptText]
|
||||
);
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
// Enter key: execute command
|
||||
terminal.writeln("");
|
||||
if (currentLineRef.current.trim()) {
|
||||
terminal.writeln(
|
||||
`\x1b[90m# Executed: ${currentLineRef.current}\x1b[0m`
|
||||
);
|
||||
terminal.writeln(
|
||||
`\x1b[33m⚠ Interactive mode not fully implemented\x1b[0m`
|
||||
);
|
||||
}
|
||||
setCurrentLine("");
|
||||
setCursorPos(0);
|
||||
terminal.write(promptText);
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Backspace key: delete character
|
||||
if (cursorPosRef.current > 0) {
|
||||
const newLine =
|
||||
currentLineRef.current.slice(0, cursorPosRef.current - 1) +
|
||||
currentLineRef.current.slice(cursorPosRef.current);
|
||||
setCurrentLine(newLine);
|
||||
setCursorPos(cursorPosRef.current - 1);
|
||||
terminal.write("\b \b"); // delete character before cursor
|
||||
}
|
||||
} else if (ev.keyCode === 37) {
|
||||
// left arrow: move cursor left
|
||||
if (cursorPosRef.current > 0) {
|
||||
setCursorPos(cursorPosRef.current - 1);
|
||||
terminal.write("\x1b[D"); // ANSI escape sequence: move cursor left
|
||||
}
|
||||
} else if (ev.keyCode === 39) {
|
||||
// right arrow: move cursor right
|
||||
if (cursorPosRef.current < currentLineRef.current.length) {
|
||||
setCursorPos(cursorPosRef.current + 1);
|
||||
terminal.write("\x1b[C"); // ANSI escape sequence: move cursor right
|
||||
}
|
||||
} else if (printable) {
|
||||
// printable character: insert at cursor position
|
||||
const newLine =
|
||||
currentLineRef.current.slice(0, cursorPosRef.current) +
|
||||
key +
|
||||
currentLineRef.current.slice(cursorPosRef.current);
|
||||
setCurrentLine(newLine);
|
||||
setCursorPos(cursorPosRef.current + 1);
|
||||
terminal.write(key);
|
||||
}
|
||||
},
|
||||
[promptText]
|
||||
);
|
||||
// initialize xterm terminal
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current || isInitialized.current) return;
|
||||
console.log('isInitialized.current', isInitialized.current);
|
||||
// mark as initialized
|
||||
isInitialized.current = true;
|
||||
|
||||
// initialize xterm terminal
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current || isInitialized.current) return;
|
||||
console.log("isInitialized.current", isInitialized.current);
|
||||
// mark as initialized
|
||||
isInitialized.current = true;
|
||||
// create terminal instance
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: 'transparent', // transparent background
|
||||
foreground: '#ffffff', // white foreground
|
||||
cursor: '#00ff00', // green cursor
|
||||
cursorAccent: '#00ff00', // cursor accent
|
||||
},
|
||||
fontFamily: '"Courier New", Courier, monospace', // monospace font
|
||||
fontSize: 12, // font size
|
||||
lineHeight: 1.2, // line height
|
||||
letterSpacing: 0, // letter spacing
|
||||
cursorBlink: true, // cursor blink
|
||||
allowProposedApi: true, // allow proposed API
|
||||
scrollback: 1000, // scrollback lines
|
||||
rightClickSelectsWord: true, // right click selects word
|
||||
smoothScrollDuration: 0, // smooth scroll duration
|
||||
fastScrollModifier: 'alt', // fast scroll modifier
|
||||
convertEol: true, // convert end of line
|
||||
windowsMode: true, // Windows mode
|
||||
cols: 100, // columns
|
||||
rows: 30, // rows
|
||||
});
|
||||
|
||||
// create terminal instance
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "transparent", // transparent background
|
||||
foreground: "#ffffff", // white foreground
|
||||
cursor: "#00ff00", // green cursor
|
||||
cursorAccent: "#00ff00", // cursor accent
|
||||
},
|
||||
fontFamily: '"Courier New", Courier, monospace', // monospace font
|
||||
fontSize: 12, // font size
|
||||
lineHeight: 1.2, // line height
|
||||
letterSpacing: 0, // letter spacing
|
||||
cursorBlink: true, // cursor blink
|
||||
allowProposedApi: true, // allow proposed API
|
||||
scrollback: 1000, // scrollback lines
|
||||
rightClickSelectsWord: true, // right click selects word
|
||||
smoothScrollDuration: 0, // smooth scroll duration
|
||||
fastScrollModifier: "alt", // fast scroll modifier
|
||||
convertEol: true, // convert end of line
|
||||
windowsMode: true, // Windows mode
|
||||
cols: 100, // columns
|
||||
rows: 30, // rows
|
||||
});
|
||||
// add plugins
|
||||
const fitAddon = new FitAddon(); // fit addon
|
||||
const webLinksAddon = new WebLinksAddon(); // web links addon
|
||||
|
||||
// add plugins
|
||||
const fitAddon = new FitAddon(); // fit addon
|
||||
const webLinksAddon = new WebLinksAddon(); // web links addon
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
// open terminal
|
||||
terminal.open(terminalRef.current);
|
||||
|
||||
// open terminal
|
||||
terminal.open(terminalRef.current);
|
||||
// wait for layout to stabilize and adapt size, then write content
|
||||
setTimeout(() => {
|
||||
fitAddon.fit(); // adapt container size
|
||||
|
||||
// wait for layout to stabilize and adapt size, then write content
|
||||
setTimeout(() => {
|
||||
fitAddon.fit(); // adapt container size
|
||||
// only show welcome information when needed
|
||||
if (showWelcome) {
|
||||
terminal.writeln('\x1b[32m=== Eigent Terminal ===\x1b[0m');
|
||||
terminal.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
|
||||
terminal.writeln('\x1b[32mReady for commands...\x1b[0m');
|
||||
terminal.writeln('');
|
||||
}
|
||||
|
||||
// only show welcome information when needed
|
||||
if (showWelcome) {
|
||||
terminal.writeln("\x1b[32m=== Eigent Terminal ===\x1b[0m");
|
||||
terminal.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
|
||||
terminal.writeln("\x1b[32mReady for commands...\x1b[0m");
|
||||
terminal.writeln("");
|
||||
}
|
||||
// show prompt
|
||||
// terminal.write(promptText);
|
||||
}, 300);
|
||||
|
||||
// show prompt
|
||||
// terminal.write(promptText);
|
||||
}, 300);
|
||||
// save reference
|
||||
xtermRef.current = terminal;
|
||||
fitAddonRef.current = fitAddon;
|
||||
|
||||
// save reference
|
||||
xtermRef.current = terminal;
|
||||
fitAddonRef.current = fitAddon;
|
||||
// add keyboard input handling
|
||||
terminal.onKey(handleKeyInput);
|
||||
|
||||
// add keyboard input handling
|
||||
terminal.onKey(handleKeyInput);
|
||||
// clean up function
|
||||
return () => {
|
||||
terminal.dispose(); // destroy terminal instance
|
||||
xtermRef.current = null;
|
||||
isInitialized.current = false;
|
||||
};
|
||||
}, [handleKeyInput, promptText, showWelcome, instanceId]);
|
||||
|
||||
// clean up function
|
||||
return () => {
|
||||
terminal.dispose(); // destroy terminal instance
|
||||
xtermRef.current = null;
|
||||
isInitialized.current = false;
|
||||
};
|
||||
}, [handleKeyInput, promptText, showWelcome, instanceId]);
|
||||
// listen to container size change
|
||||
useEffect(() => {
|
||||
if (!terminalContainerRef.current || !fitAddonRef.current) return;
|
||||
|
||||
// listen to container size change
|
||||
useEffect(() => {
|
||||
if (!terminalContainerRef.current || !fitAddonRef.current) return;
|
||||
// use ResizeObserver to listen to container size change
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const _entry of entries) {
|
||||
// delay execution of fit to ensure layout stability
|
||||
setTimeout(() => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// use ResizeObserver to listen to container size change
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
// delay execution of fit to ensure layout stability
|
||||
setTimeout(() => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(terminalContainerRef.current);
|
||||
|
||||
resizeObserver.observe(terminalContainerRef.current);
|
||||
// listen to window size change
|
||||
const handleResize = () => {
|
||||
setTimeout(() => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// listen to window size change
|
||||
const handleResize = () => {
|
||||
setTimeout(() => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
// clean up listeners
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// clean up listeners
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
// listen to terminal data change and write to xterm
|
||||
useEffect(() => {
|
||||
if (!xtermRef.current || !content) return;
|
||||
const terminalData = content;
|
||||
const currentLength = terminalData.length;
|
||||
|
||||
// listen to terminal data change and write to xterm
|
||||
useEffect(() => {
|
||||
if (!xtermRef.current || !content) return;
|
||||
const terminalData = content;
|
||||
const currentLength = terminalData.length;
|
||||
// check if it is the case of component re-initialization
|
||||
// if lastTerminalLength is 0 but content has data, it means re-initialization
|
||||
if (lastTerminalLength.current === 0 && currentLength > 0) {
|
||||
console.log('component re-initialization, skip history data write');
|
||||
lastTerminalLength.current = currentLength;
|
||||
return;
|
||||
}
|
||||
|
||||
// check if it is the case of component re-initialization
|
||||
// if lastTerminalLength is 0 but content has data, it means re-initialization
|
||||
if (lastTerminalLength.current === 0 && currentLength > 0) {
|
||||
console.log("component re-initialization, skip history data write");
|
||||
lastTerminalLength.current = currentLength;
|
||||
return;
|
||||
}
|
||||
// only process new data (incremental update)
|
||||
if (currentLength > lastTerminalLength.current) {
|
||||
const newData = terminalData.slice(lastTerminalLength.current);
|
||||
|
||||
// only process new data (incremental update)
|
||||
if (currentLength > lastTerminalLength.current) {
|
||||
const newData = terminalData.slice(lastTerminalLength.current);
|
||||
console.log('newData', newData);
|
||||
newData.forEach((item) => {
|
||||
if (!xtermRef.current) return;
|
||||
|
||||
console.log("newData", newData);
|
||||
newData.forEach((item) => {
|
||||
if (!xtermRef.current) return;
|
||||
// move to line head and clear whole line
|
||||
xtermRef.current.write('\r');
|
||||
xtermRef.current.write('\x1b[2K'); // clear whole line
|
||||
|
||||
// move to line head and clear whole line
|
||||
xtermRef.current.write("\r");
|
||||
xtermRef.current.write("\x1b[2K"); // clear whole line
|
||||
// process output content
|
||||
const formattedOutput = item
|
||||
.replace(/\r?\n$/, '') // remove trailing newline
|
||||
.replace(/\t/g, ' ') // convert tab to 4 spaces
|
||||
.replace(/\r/g, ''); // remove carriage return
|
||||
|
||||
// process output content
|
||||
const formattedOutput = item
|
||||
.replace(/\r?\n$/, "") // remove trailing newline
|
||||
.replace(/\t/g, " ") // convert tab to 4 spaces
|
||||
.replace(/\r/g, ""); // remove carriage return
|
||||
if (formattedOutput.trim()) {
|
||||
xtermRef.current.writeln(
|
||||
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
|
||||
);
|
||||
} else {
|
||||
xtermRef.current.writeln('');
|
||||
}
|
||||
|
||||
if (formattedOutput.trim()) {
|
||||
xtermRef.current.writeln(`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`);
|
||||
} else {
|
||||
xtermRef.current.writeln("");
|
||||
}
|
||||
// re-display prompt
|
||||
xtermRef.current.write(promptText);
|
||||
|
||||
// re-display prompt
|
||||
xtermRef.current.write(promptText);
|
||||
// re-display current input
|
||||
if (currentLineRef.current) {
|
||||
xtermRef.current.write(currentLineRef.current);
|
||||
|
||||
// re-display current input
|
||||
if (currentLineRef.current) {
|
||||
xtermRef.current.write(currentLineRef.current);
|
||||
// if cursor is not at the end, move to the correct position
|
||||
if (cursorPosRef.current < currentLineRef.current.length) {
|
||||
const moveBack =
|
||||
currentLineRef.current.length - cursorPosRef.current;
|
||||
for (let i = 0; i < moveBack; i++) {
|
||||
xtermRef.current.write('\x1b[D'); // move cursor left
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if cursor is not at the end, move to the correct position
|
||||
if (cursorPosRef.current < currentLineRef.current.length) {
|
||||
const moveBack =
|
||||
currentLineRef.current.length - cursorPosRef.current;
|
||||
for (let i = 0; i < moveBack; i++) {
|
||||
xtermRef.current.write("\x1b[D"); // move cursor left
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
lastTerminalLength.current = currentLength;
|
||||
}
|
||||
}, [content, promptText]);
|
||||
|
||||
lastTerminalLength.current = currentLength;
|
||||
}
|
||||
}, [content, promptText]);
|
||||
// reset terminal when switching task
|
||||
useEffect(() => {
|
||||
if (!xtermRef.current) return;
|
||||
|
||||
// reset terminal when switching task
|
||||
useEffect(() => {
|
||||
if (!xtermRef.current) return;
|
||||
// clear terminal
|
||||
xtermRef.current.clear();
|
||||
|
||||
// clear terminal
|
||||
xtermRef.current.clear();
|
||||
// reset state
|
||||
lastTerminalLength.current = 0;
|
||||
setCurrentLine('');
|
||||
setCursorPos(0);
|
||||
|
||||
// reset state
|
||||
lastTerminalLength.current = 0;
|
||||
setCurrentLine("");
|
||||
setCursorPos(0);
|
||||
// delay re-initialization
|
||||
setTimeout(() => {
|
||||
if (!xtermRef.current || !fitAddonRef.current) return;
|
||||
|
||||
// delay re-initialization
|
||||
setTimeout(() => {
|
||||
if (!xtermRef.current || !fitAddonRef.current) return;
|
||||
// re-adapt size
|
||||
fitAddonRef.current.fit();
|
||||
|
||||
// re-adapt size
|
||||
fitAddonRef.current.fit();
|
||||
// only show switch information on main instance
|
||||
if (showWelcome) {
|
||||
xtermRef.current.writeln('\x1b[32m=== Eigent Terminal ===\x1b[0m');
|
||||
xtermRef.current.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
|
||||
xtermRef.current.writeln('\x1b[32mTask switched...\x1b[0m');
|
||||
xtermRef.current.writeln('');
|
||||
}
|
||||
|
||||
// only show switch information on main instance
|
||||
if (showWelcome) {
|
||||
xtermRef.current.writeln("\x1b[32m=== Eigent Terminal ===\x1b[0m");
|
||||
xtermRef.current.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
|
||||
xtermRef.current.writeln("\x1b[32mTask switched...\x1b[0m");
|
||||
xtermRef.current.writeln("");
|
||||
}
|
||||
// if there is history data, re-write
|
||||
if (chatStore.activeTaskId) {
|
||||
const terminalData = content || [];
|
||||
if (terminalData.length > 0) {
|
||||
xtermRef.current.writeln('\x1b[90m--- Previous Output ---\x1b[0m');
|
||||
terminalData.forEach((item) => {
|
||||
const formattedOutput = item
|
||||
.replace(/\r?\n$/, '')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/\r/g, '');
|
||||
|
||||
// if there is history data, re-write
|
||||
if (chatStore.activeTaskId) {
|
||||
const terminalData = content || [];
|
||||
if (terminalData.length > 0) {
|
||||
xtermRef.current.writeln("\x1b[90m--- Previous Output ---\x1b[0m");
|
||||
terminalData.forEach((item) => {
|
||||
const formattedOutput = item
|
||||
.replace(/\r?\n$/, "")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/\r/g, "");
|
||||
if (formattedOutput.trim()) {
|
||||
xtermRef.current?.writeln(
|
||||
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
|
||||
);
|
||||
}
|
||||
});
|
||||
xtermRef.current.writeln(
|
||||
'\x1b[90m--- End Previous Output ---\x1b[0m'
|
||||
);
|
||||
xtermRef.current.writeln('');
|
||||
}
|
||||
lastTerminalLength.current = terminalData.length;
|
||||
}
|
||||
|
||||
if (formattedOutput.trim()) {
|
||||
xtermRef.current?.writeln(
|
||||
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
|
||||
);
|
||||
}
|
||||
});
|
||||
xtermRef.current.writeln(
|
||||
"\x1b[90m--- End Previous Output ---\x1b[0m"
|
||||
);
|
||||
xtermRef.current.writeln("");
|
||||
}
|
||||
lastTerminalLength.current = terminalData.length;
|
||||
}
|
||||
// show prompt
|
||||
xtermRef.current.write(promptText);
|
||||
}, 200);
|
||||
}, [chatStore.activeTaskId, showWelcome, instanceId, content]);
|
||||
|
||||
// show prompt
|
||||
xtermRef.current.write(promptText);
|
||||
}, 200);
|
||||
}, [chatStore.activeTaskId, showWelcome, instanceId]);
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
// render terminal component
|
||||
return (
|
||||
<div
|
||||
ref={terminalContainerRef}
|
||||
className="w-full h-full flex flex-col rounded-2xl border border-border-subtle-strong border-solid relative overflow-hidden"
|
||||
style={{ fontFamily: '"Courier New", Courier, monospace' }}
|
||||
>
|
||||
{/* background blur effect */}
|
||||
<div className="absolute inset-0 blur-bg pointer-events-none bg-black-100% rounded-xl"></div>
|
||||
// render terminal component
|
||||
return (
|
||||
<div
|
||||
ref={terminalContainerRef}
|
||||
className="relative flex h-full w-full flex-col overflow-hidden rounded-2xl border border-solid border-border-subtle-strong"
|
||||
style={{ fontFamily: '"Courier New", Courier, monospace' }}
|
||||
>
|
||||
{/* background blur effect */}
|
||||
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-black-100%"></div>
|
||||
|
||||
{/* terminal container */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
margin: "16px",
|
||||
width: "calc(100% - 32px)",
|
||||
height: "calc(100% - 32px)",
|
||||
fontFamily: '"Courier New", Courier, monospace',
|
||||
}}
|
||||
/>
|
||||
{/* terminal container */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
margin: '16px',
|
||||
width: 'calc(100% - 32px)',
|
||||
height: 'calc(100% - 32px)',
|
||||
fontFamily: '"Courier New", Courier, monospace',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* custom style: override xterm.js character spacing */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
{/* custom style: override xterm.js character spacing */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.xterm span {
|
||||
letter-spacing: 0.5px !important;
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export default function TerminalAgentWrokSpace() {
|
|||
className="rounded-full"
|
||||
>
|
||||
<ChevronLeft size={16} className="text-text-inverse-primary" />
|
||||
<span className="text-text-inverse-primary text-sm font-bold leading-13">
|
||||
<span className="text-sm font-bold leading-13 text-text-inverse-primary">
|
||||
{t('chat.give-back-to-agent')}
|
||||
</span>
|
||||
</Button>
|
||||
|
|
@ -172,7 +172,7 @@ export default function TerminalAgentWrokSpace() {
|
|||
{agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-text-tertiary text-[10px] font-medium leading-17">
|
||||
<div className="text-[10px] font-medium leading-17 text-text-tertiary">
|
||||
{
|
||||
activeAgent?.tasks?.filter(
|
||||
(task) => task.status && task.status !== 'running'
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ import { TooltipSimple } from '@/components/ui/tooltip';
|
|||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { share } from '@/lib/share';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useInstallationUI } from '@/store/installationStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
|
|
@ -45,7 +47,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
|
||||
function HeaderWin() {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -60,6 +61,10 @@ function HeaderWin() {
|
|||
const appearance = useAuthStore((state) => state.appearance);
|
||||
const [endDialogOpen, setEndDialogOpen] = useState(false);
|
||||
const [endProjectLoading, setEndProjectLoading] = useState(false);
|
||||
const { isInstalling, installationState } = useInstallationUI();
|
||||
const _isInstallationActive =
|
||||
isInstalling || installationState === 'waiting-backend';
|
||||
|
||||
useEffect(() => {
|
||||
const p = window.electronAPI.getPlatform();
|
||||
setPlatform(p);
|
||||
|
|
@ -91,22 +96,15 @@ function HeaderWin() {
|
|||
navigate('/');
|
||||
};
|
||||
|
||||
const summaryTask =
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.summaryTask;
|
||||
|
||||
const activeTaskTitle = useMemo(() => {
|
||||
if (
|
||||
chatStore?.activeTaskId &&
|
||||
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask
|
||||
) {
|
||||
return chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
].summaryTask.split('|')[0];
|
||||
if (chatStore?.activeTaskId && summaryTask) {
|
||||
return summaryTask.split('|')[0];
|
||||
}
|
||||
return t('layout.new-project');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
chatStore?.activeTaskId,
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.summaryTask,
|
||||
t,
|
||||
]);
|
||||
}, [chatStore?.activeTaskId, summaryTask, t]);
|
||||
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
|
|
|
|||
|
|
@ -12,202 +12,202 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { isHtmlDocument } from "@/lib/htmlFontStyles";
|
||||
import { isHtmlDocument } from '@/lib/htmlFontStyles';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
export const MarkDown = ({
|
||||
content,
|
||||
speed = 15,
|
||||
onTyping,
|
||||
enableTypewriter = true, // Whether to enable typewriter effect
|
||||
pTextSize = "text-xs",
|
||||
olPadding = "",
|
||||
content,
|
||||
speed = 15,
|
||||
onTyping,
|
||||
enableTypewriter = true, // Whether to enable typewriter effect
|
||||
pTextSize = 'text-xs',
|
||||
olPadding = '',
|
||||
}: {
|
||||
content: string;
|
||||
speed?: number;
|
||||
onTyping?: () => void;
|
||||
enableTypewriter?: boolean;
|
||||
pTextSize?: string;
|
||||
olPadding?: string;
|
||||
content: string;
|
||||
speed?: number;
|
||||
onTyping?: () => void;
|
||||
enableTypewriter?: boolean;
|
||||
pTextSize?: string;
|
||||
olPadding?: string;
|
||||
}) => {
|
||||
const [displayedContent, setDisplayedContent] = useState("");
|
||||
const [displayedContent, setDisplayedContent] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTypewriter) {
|
||||
setDisplayedContent(content);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!enableTypewriter) {
|
||||
setDisplayedContent(content);
|
||||
return;
|
||||
}
|
||||
|
||||
setDisplayedContent("");
|
||||
let index = 0;
|
||||
setDisplayedContent('');
|
||||
let index = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (index < content.length) {
|
||||
setDisplayedContent(content.slice(0, index + 1));
|
||||
index++;
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, speed);
|
||||
const timer = setInterval(() => {
|
||||
if (index < content.length) {
|
||||
setDisplayedContent(content.slice(0, index + 1));
|
||||
index++;
|
||||
if (onTyping) {
|
||||
onTyping();
|
||||
}
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, speed);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [content, speed]);
|
||||
return () => clearInterval(timer);
|
||||
}, [content, speed, enableTypewriter, onTyping]);
|
||||
|
||||
// process line breaks, convert \n to <br> tag
|
||||
const processContent = (text: string) => {
|
||||
return text.replace(/\\n/g, " \n "); // add two spaces before \n, so ReactMarkdown will recognize it as a line break
|
||||
};
|
||||
// process line breaks, convert \n to <br> tag
|
||||
const processContent = (text: string) => {
|
||||
return text.replace(/\\n/g, ' \n '); // add two spaces before \n, so ReactMarkdown will recognize it as a line break
|
||||
};
|
||||
|
||||
// If content is a pure HTML document, render in a styled pre block
|
||||
if (isHtmlDocument(content)) {
|
||||
// Trim leading whitespace from each line for consistent alignment
|
||||
const formattedHtml = displayedContent
|
||||
.split('\n')
|
||||
.map(line => line.trimStart())
|
||||
.join('\n')
|
||||
.trim();
|
||||
return (
|
||||
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto markdown-container">
|
||||
<pre className="bg-code-surface p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
|
||||
<code>{formattedHtml}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If content is a pure HTML document, render in a styled pre block
|
||||
if (isHtmlDocument(content)) {
|
||||
// Trim leading whitespace from each line for consistent alignment
|
||||
const formattedHtml = displayedContent
|
||||
.split('\n')
|
||||
.map((line) => line.trimStart())
|
||||
.join('\n')
|
||||
.trim();
|
||||
return (
|
||||
<div className="prose prose-sm markdown-container pointer-events-auto w-full select-text overflow-x-auto">
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-code-surface p-2 font-mono text-xs">
|
||||
<code>{formattedHtml}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto markdown-container">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-xs font-bold text-primary mb-1 break-words">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-xs font-semibold text-primary mb-1 break-words">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xs font-medium text-primary mb-1 break-words">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p
|
||||
className={`m-0 ${pTextSize} font-medium text-xs text-primary leading-10 font-inter whitespace-pre-line break-words`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul
|
||||
className={`list-disc list-inside text-xs text-primary mb-1 ${olPadding}`}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
// ol: ({ children }) => (
|
||||
// <ol
|
||||
// className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
|
||||
// >
|
||||
// {children}
|
||||
// </ol>
|
||||
// ),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 list-inside break-all">{children}</li>
|
||||
),
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
className=" hover:text-text-link-hover underline break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="bg-code-surface px-1 py-0.5 rounded text-xs font-mono">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-code-surface p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-border-subtle-strong pl-3 italic text-primary text-xs">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-primary text-xs">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic text-primary text-xs">{children}</em>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto w-full max-w-full">
|
||||
<table
|
||||
className="w-full mb-4 !table min-w-0"
|
||||
style={{
|
||||
borderCollapse: "collapse",
|
||||
border: "1px solid #d1d5db",
|
||||
borderSpacing: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="!table-header-group bg-code-surface">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="!table-row-group">{children}</tbody>
|
||||
),
|
||||
tr: ({ children }) => <tr className="!table-row">{children}</tr>,
|
||||
th: ({ children }) => (
|
||||
<th
|
||||
className="text-left font-semibold text-primary text-[10px] !table-cell"
|
||||
style={{
|
||||
border: "1px solid #d1d5db",
|
||||
padding: "2px 5px",
|
||||
borderCollapse: "collapse",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td
|
||||
className="text-primary text-[10px] !table-cell"
|
||||
style={{
|
||||
border: "1px solid #d1d5db",
|
||||
padding: "2px 5px",
|
||||
borderCollapse: "collapse",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{processContent(displayedContent)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="prose prose-sm markdown-container pointer-events-auto w-full select-text overflow-x-auto">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-primary mb-1 break-words text-xs font-bold">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-primary mb-1 break-words text-xs font-semibold">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-primary mb-1 break-words text-xs font-medium">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p
|
||||
className={`m-0 ${pTextSize} text-primary whitespace-pre-line break-words font-inter text-xs font-medium leading-10`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul
|
||||
className={`text-primary mb-1 list-inside list-disc text-xs ${olPadding}`}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
// ol: ({ children }) => (
|
||||
// <ol
|
||||
// className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
|
||||
// >
|
||||
// {children}
|
||||
// </ol>
|
||||
// ),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 list-inside break-all">{children}</li>
|
||||
),
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="break-all underline hover:text-text-link-hover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="rounded bg-code-surface px-1 py-0.5 font-mono text-xs">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-code-surface p-2 font-mono text-xs">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="text-primary border-l-4 border-border-subtle-strong pl-3 text-xs italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="text-primary text-xs font-semibold">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="text-primary text-xs italic">{children}</em>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="w-full max-w-full overflow-x-auto">
|
||||
<table
|
||||
className="mb-4 !table w-full min-w-0"
|
||||
style={{
|
||||
borderCollapse: 'collapse',
|
||||
border: '1px solid #d1d5db',
|
||||
borderSpacing: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="!table-header-group bg-code-surface">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="!table-row-group">{children}</tbody>
|
||||
),
|
||||
tr: ({ children }) => <tr className="!table-row">{children}</tr>,
|
||||
th: ({ children }) => (
|
||||
<th
|
||||
className="text-primary !table-cell text-left text-[10px] font-semibold"
|
||||
style={{
|
||||
border: '1px solid #d1d5db',
|
||||
padding: '2px 5px',
|
||||
borderCollapse: 'collapse',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td
|
||||
className="text-primary !table-cell text-[10px]"
|
||||
style={{
|
||||
border: '1px solid #d1d5db',
|
||||
padding: '2px 5px',
|
||||
borderCollapse: 'collapse',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{processContent(displayedContent)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default function ConfirmModal({
|
|||
className="alert-dialog-wrapper fixed max-w-md rounded-xl shadow-perfect"
|
||||
>
|
||||
<div className="p-6">
|
||||
<span className="text-text-primary mb-2 text-body-lg font-bold">
|
||||
<span className="mb-2 text-body-lg font-bold text-text-primary">
|
||||
{title}
|
||||
</span>
|
||||
<p className="mb-6 text-label-md text-text-label">{message}</p>
|
||||
|
|
|
|||
|
|
@ -24,13 +24,18 @@ const ProgressInstall = React.forwardRef<
|
|||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-specialty-progress-surface relative h-3 w-full overflow-hidden rounded-full',
|
||||
'relative h-3 w-full overflow-hidden rounded-full bg-surface-tertiary',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* Shimmer background layer */}
|
||||
<div className="progress-install-shimmer" />
|
||||
|
||||
<ProgressPrimitive.Indicator
|
||||
className={`h-full w-full flex-1 transition-all`}
|
||||
className={cn(
|
||||
'relative z-10 h-full w-full flex-1 rounded-full transition-all'
|
||||
)}
|
||||
style={{
|
||||
transform: `translateX(-${100 - (value || 0)}%)`,
|
||||
background:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
proxyFetchPost,
|
||||
proxyFetchPut,
|
||||
} from '@/api/http';
|
||||
import { capitalizeFirstLetter } from '@/lib';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
"eigent-cloud": "Eigent Cloud",
|
||||
"default": "默认",
|
||||
"profile": "个人资料",
|
||||
|
||||
"account": "账户",
|
||||
"you-are-currently-signed-in-with": "你当前使用的是 <email>{{email}}</email> 账户",
|
||||
"you-are-currently-signed-in-with": "你当前使用的是 {{email}} 账户",
|
||||
"manage": "管理",
|
||||
"log-out": "退出",
|
||||
"language": "语言",
|
||||
|
|
@ -102,7 +103,6 @@
|
|||
"save": "保存",
|
||||
"confirm-delete": "确认删除",
|
||||
"are-you-sure-you-want-to-delete": "你确定要删除",
|
||||
"deleting": "删除中...",
|
||||
"delete": "删除",
|
||||
"configure {name} Toolkit": "配置 {{name}} 工具包",
|
||||
"get-it-from": "获取它来自",
|
||||
|
|
@ -163,7 +163,6 @@
|
|||
"reset": "重置",
|
||||
"reset-success": "重置成功!",
|
||||
"reset-failed": "重置失败!",
|
||||
"select-default-model": "选择默认模型",
|
||||
|
||||
"browser-login": "浏览器登录",
|
||||
"browser-login-description": "打开 Chrome 浏览器以登录您的账户。您的登录数据将安全地保存在本地配置文件中。",
|
||||
|
|
@ -192,14 +191,6 @@
|
|||
"all-cookies-deleted": "所有 Cookie 已成功删除。",
|
||||
"cookie-delete-warning": "注意:删除 Cookie 会使您从相关网站登出。您可能需要重启浏览器才能看到更改生效。",
|
||||
|
||||
"network-proxy": "网络代理",
|
||||
"network-proxy-description": "配置网络请求的代理服务器。如果您需要通过代理访问外部 API,这将非常有用。",
|
||||
"proxy-placeholder": "http://127.0.0.1:7890",
|
||||
"proxy-saved-restart-required": "代理配置已保存。请重启应用以应用更改。",
|
||||
"proxy-save-failed": "保存代理配置失败。",
|
||||
"proxy-invalid-url": "无效的代理 URL。必须以 http://、https://、socks4:// 或 socks5:// 开头。",
|
||||
"proxy-restart-hint": "需要重启应用以应用代理更改。",
|
||||
|
||||
"cloud-not-available-in-local-proxy": "在本地代理模式下无法使用云端版本",
|
||||
"set-as-default": "设为默认",
|
||||
"api-key-setting": "API 密钥设置",
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@
|
|||
"privacy": "隱私",
|
||||
"models": "模型",
|
||||
"mcp": "MCP & 工具",
|
||||
"eigent-cloud": "Eigent Cloud",
|
||||
"default": "預設",
|
||||
"profile": "個人資料",
|
||||
|
||||
"account": "帳戶",
|
||||
"you-are-currently-signed-in-with": "您目前使用的是 <email>{{email}}</email> 帳戶",
|
||||
"you-are-currently-signed-in-with": "您目前使用的是 {{email}} 帳戶",
|
||||
"manage": "管理",
|
||||
"log-out": "登出",
|
||||
"language": "語言",
|
||||
|
|
@ -164,14 +162,6 @@
|
|||
"reset-success": "重設成功!",
|
||||
"reset-failed": "重設失敗!",
|
||||
"select-default-model": "選擇預設模型",
|
||||
"network-proxy": "網路代理",
|
||||
"network-proxy-description": "設定網路請求的代理伺服器。如果您需要透過代理存取外部 API,這將非常有用。",
|
||||
"proxy-placeholder": "http://127.0.0.1:7890",
|
||||
"proxy-saved-restart-required": "代理設定已儲存。請重新啟動應用程式以套用變更。",
|
||||
"proxy-save-failed": "儲存代理設定失敗。",
|
||||
"proxy-invalid-url": "無效的代理 URL。必須以 http://、https://、socks4:// 或 socks5:// 開頭。",
|
||||
"proxy-restart-hint": "需要重新啟動應用程式以套用代理變更。",
|
||||
|
||||
"cloud-not-available-in-local-proxy": "在本機代理模式下無法使用雲端版本",
|
||||
"set-as-default": "設為預設",
|
||||
"api-key-setting": "API 金鑰設定",
|
||||
|
|
@ -194,6 +184,13 @@
|
|||
"gpt-5-mini-name": "GPT-5 Mini",
|
||||
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
|
||||
|
||||
"network-proxy": "網路代理",
|
||||
"network-proxy-description": "設定網路請求的代理伺服器。如果您需要透過代理存取外部 API,這將非常有用。",
|
||||
"proxy-placeholder": "http://127.0.0.1:7890",
|
||||
"proxy-saved-restart-required": "代理設定已儲存。請重新啟動應用程式以套用變更。",
|
||||
"proxy-save-failed": "儲存代理設定失敗。",
|
||||
"proxy-invalid-url": "無效的代理 URL。必須以 http://、https://、socks4:// 或 socks5:// 開頭。",
|
||||
"proxy-restart-hint": "需要重新啟動應用程式以套用代理變更。",
|
||||
"preferred-ide": "偏好 IDE",
|
||||
"preferred-ide-description": "選擇開啟智能體專案資料夾時使用的應用程式。",
|
||||
"system-file-manager": "系統檔案管理器"
|
||||
|
|
|
|||
|
|
@ -12,303 +12,308 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { ChatTaskStatus } from "@/types/constants";
|
||||
import ChatBox from "@/components/ChatBox";
|
||||
import Workflow from "@/components/WorkFlow";
|
||||
import Folder from "@/components/Folder";
|
||||
import Terminal from "@/components/Terminal";
|
||||
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import BottomBar from "@/components/BottomBar";
|
||||
import BrowserAgentWorkSpace from "@/components/BrowserAgentWorkSpace";
|
||||
import TerminalAgentWrokSpace from "@/components/TerminalAgentWrokSpace";
|
||||
import UpdateElectron from "@/components/update";
|
||||
import BottomBar from '@/components/BottomBar';
|
||||
import BrowserAgentWorkSpace from '@/components/BrowserAgentWorkSpace';
|
||||
import ChatBox from '@/components/ChatBox';
|
||||
import Folder from '@/components/Folder';
|
||||
import TerminalAgentWrokSpace from '@/components/TerminalAgentWrokSpace';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@/components/ui/resizable';
|
||||
import UpdateElectron from '@/components/update';
|
||||
import Workflow from '@/components/WorkFlow';
|
||||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function Home() {
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore, projectStore } = useChatStoreAdapter();
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore, projectStore } = useChatStoreAdapter();
|
||||
|
||||
const [_activeWebviewId, setActiveWebviewId] = useState<string | null>(null);
|
||||
const [_activeWebviewId, setActiveWebviewId] = useState<string | null>(null);
|
||||
|
||||
// Add webview-show listener in useEffect with cleanup
|
||||
useEffect(() => {
|
||||
const handleWebviewShow = (_event: any, id: string) => {
|
||||
setActiveWebviewId(id);
|
||||
};
|
||||
// Add webview-show listener in useEffect with cleanup
|
||||
useEffect(() => {
|
||||
const handleWebviewShow = (_event: any, id: string) => {
|
||||
setActiveWebviewId(id);
|
||||
};
|
||||
|
||||
window.ipcRenderer?.on("webview-show", handleWebviewShow);
|
||||
window.ipcRenderer?.on('webview-show', handleWebviewShow);
|
||||
|
||||
// Cleanup: remove listener on unmount
|
||||
return () => {
|
||||
window.ipcRenderer?.off("webview-show", handleWebviewShow);
|
||||
};
|
||||
}, []); // Empty dependency array means this only runs once
|
||||
// Cleanup: remove listener on unmount
|
||||
return () => {
|
||||
window.ipcRenderer?.off('webview-show', handleWebviewShow);
|
||||
};
|
||||
}, []); // Empty dependency array means this only runs once
|
||||
|
||||
// Extract complex dependency to a variable
|
||||
const taskAssigning =
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning;
|
||||
// Extract complex dependency to a variable
|
||||
const taskAssigning =
|
||||
chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning;
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatStore) return;
|
||||
useEffect(() => {
|
||||
if (!chatStore) return;
|
||||
|
||||
let taskAssigningArray = [
|
||||
...(taskAssigning || []),
|
||||
];
|
||||
let webviews: { id: string; agent_id: string; index: number }[] = [];
|
||||
taskAssigningArray.map((item) => {
|
||||
if (item.type === "browser_agent") {
|
||||
item.activeWebviewIds?.map((webview, index) => {
|
||||
webviews.push({ ...webview, agent_id: item.agent_id, index });
|
||||
});
|
||||
}
|
||||
});
|
||||
let taskAssigningArray = [...(taskAssigning || [])];
|
||||
let webviews: { id: string; agent_id: string; index: number }[] = [];
|
||||
taskAssigningArray.map((item) => {
|
||||
if (item.type === 'browser_agent') {
|
||||
item.activeWebviewIds?.map((webview, index) => {
|
||||
webviews.push({ ...webview, agent_id: item.agent_id, index });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (taskAssigningArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (taskAssigningArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (webviews.length === 0) {
|
||||
const browserAgent = taskAssigningArray.find(agent => agent.type === 'browser_agent');
|
||||
if (browserAgent && browserAgent.activeWebviewIds && browserAgent.activeWebviewIds.length > 0) {
|
||||
browserAgent.activeWebviewIds.forEach((webview, index) => {
|
||||
webviews.push({ ...webview, agent_id: browserAgent.agent_id, index });
|
||||
});
|
||||
}
|
||||
}
|
||||
if (webviews.length === 0) {
|
||||
const browserAgent = taskAssigningArray.find(
|
||||
(agent) => agent.type === 'browser_agent'
|
||||
);
|
||||
if (
|
||||
browserAgent &&
|
||||
browserAgent.activeWebviewIds &&
|
||||
browserAgent.activeWebviewIds.length > 0
|
||||
) {
|
||||
browserAgent.activeWebviewIds.forEach((webview, index) => {
|
||||
webviews.push({ ...webview, agent_id: browserAgent.agent_id, index });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (webviews.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (webviews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// capture webview
|
||||
const captureWebview = async () => {
|
||||
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!activeTask || activeTask.status === ChatTaskStatus.FINISHED) {
|
||||
return;
|
||||
}
|
||||
webviews.map((webview) => {
|
||||
window.ipcRenderer
|
||||
.invoke("capture-webview", webview.id)
|
||||
.then((base64: string) => {
|
||||
const currentTask = chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!currentTask || currentTask.type) return;
|
||||
let taskAssigning = [
|
||||
...currentTask.taskAssigning,
|
||||
];
|
||||
const browserAgentIndex = taskAssigning.findIndex(
|
||||
(agent) => agent.agent_id === webview.agent_id
|
||||
);
|
||||
// capture webview
|
||||
const captureWebview = async () => {
|
||||
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!activeTask || activeTask.status === ChatTaskStatus.FINISHED) {
|
||||
return;
|
||||
}
|
||||
webviews.map((webview) => {
|
||||
window.ipcRenderer
|
||||
.invoke('capture-webview', webview.id)
|
||||
.then((base64: string) => {
|
||||
const currentTask =
|
||||
chatStore.tasks[chatStore.activeTaskId as string];
|
||||
if (!currentTask || currentTask.type) return;
|
||||
let taskAssigning = [...currentTask.taskAssigning];
|
||||
const browserAgentIndex = taskAssigning.findIndex(
|
||||
(agent) => agent.agent_id === webview.agent_id
|
||||
);
|
||||
|
||||
if (
|
||||
browserAgentIndex !== -1 &&
|
||||
base64 !== "data:image/jpeg;base64,"
|
||||
) {
|
||||
taskAssigning[browserAgentIndex].activeWebviewIds![
|
||||
webview.index
|
||||
].img = base64;
|
||||
chatStore.setTaskAssigning(
|
||||
chatStore.activeTaskId as string,
|
||||
taskAssigning
|
||||
);
|
||||
const { processTaskId, url } =
|
||||
taskAssigning[browserAgentIndex].activeWebviewIds![
|
||||
webview.index
|
||||
];
|
||||
chatStore.setSnapshotsTemp(chatStore.activeTaskId as string, {
|
||||
api_task_id: chatStore.activeTaskId,
|
||||
camel_task_id: processTaskId,
|
||||
browser_url: url,
|
||||
image_base64: base64,
|
||||
});
|
||||
if (
|
||||
browserAgentIndex !== -1 &&
|
||||
base64 !== 'data:image/jpeg;base64,'
|
||||
) {
|
||||
taskAssigning[browserAgentIndex].activeWebviewIds![
|
||||
webview.index
|
||||
].img = base64;
|
||||
chatStore.setTaskAssigning(
|
||||
chatStore.activeTaskId as string,
|
||||
taskAssigning
|
||||
);
|
||||
const { processTaskId, url } =
|
||||
taskAssigning[browserAgentIndex].activeWebviewIds![
|
||||
webview.index
|
||||
];
|
||||
chatStore.setSnapshotsTemp(chatStore.activeTaskId as string, {
|
||||
api_task_id: chatStore.activeTaskId,
|
||||
camel_task_id: processTaskId,
|
||||
browser_url: url,
|
||||
image_base64: base64,
|
||||
});
|
||||
}
|
||||
// let list: any = [];
|
||||
// taskAssigning.forEach((item: any) => {
|
||||
// item.activeWebviewIds.forEach((item2: any) => {
|
||||
// if (item2.img && item2.url && item2.processTaskId) {
|
||||
// list.push({
|
||||
// api_task_id: chatStore.activeTaskId,
|
||||
// camel_task_id: item2.processTaskId,
|
||||
// browser_url: item2.url,
|
||||
// image_base64: item2.img,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// chatStore.setSnapshots(chatStore.activeTaskId as string, list);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('capture webview error:', error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
// let list: any = [];
|
||||
// taskAssigning.forEach((item: any) => {
|
||||
// item.activeWebviewIds.forEach((item2: any) => {
|
||||
// if (item2.img && item2.url && item2.processTaskId) {
|
||||
// list.push({
|
||||
// api_task_id: chatStore.activeTaskId,
|
||||
// camel_task_id: item2.processTaskId,
|
||||
// browser_url: item2.url,
|
||||
// image_base64: item2.img,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// chatStore.setSnapshots(chatStore.activeTaskId as string, list);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("capture webview error:", error);
|
||||
});
|
||||
});
|
||||
};
|
||||
let intervalTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
let intervalTimer: NodeJS.Timeout | null = null;
|
||||
const initialTimer = setTimeout(() => {
|
||||
captureWebview();
|
||||
intervalTimer = setInterval(captureWebview, 2000);
|
||||
}, 2000);
|
||||
|
||||
const initialTimer = setTimeout(() => {
|
||||
captureWebview();
|
||||
intervalTimer = setInterval(captureWebview, 2000);
|
||||
}, 2000);
|
||||
// cleanup function
|
||||
return () => {
|
||||
clearTimeout(initialTimer);
|
||||
if (intervalTimer) {
|
||||
clearInterval(intervalTimer);
|
||||
}
|
||||
};
|
||||
}, [chatStore, taskAssigning]);
|
||||
|
||||
// cleanup function
|
||||
return () => {
|
||||
clearTimeout(initialTimer);
|
||||
if (intervalTimer) {
|
||||
clearInterval(intervalTimer);
|
||||
}
|
||||
};
|
||||
}, [chatStore, taskAssigning]);
|
||||
const getSize = useCallback(() => {
|
||||
const webviewContainer = document.getElementById('webview-container');
|
||||
if (webviewContainer) {
|
||||
const rect = webviewContainer.getBoundingClientRect();
|
||||
window.electronAPI.setSize({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
console.log('setSize', rect);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getSize = useCallback(() => {
|
||||
const webviewContainer = document.getElementById("webview-container");
|
||||
if (webviewContainer) {
|
||||
const rect = webviewContainer.getBoundingClientRect();
|
||||
window.electronAPI.setSize({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
console.log("setSize", rect);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!chatStore) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatStore) return;
|
||||
if (!chatStore.activeTaskId) {
|
||||
projectStore?.createProject('new project');
|
||||
}
|
||||
|
||||
if (!chatStore.activeTaskId) {
|
||||
projectStore?.createProject("new project");
|
||||
}
|
||||
const webviewContainer = document.getElementById('webview-container');
|
||||
if (webviewContainer) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
getSize();
|
||||
});
|
||||
resizeObserver.observe(webviewContainer);
|
||||
|
||||
const webviewContainer = document.getElementById("webview-container");
|
||||
if (webviewContainer) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
getSize();
|
||||
});
|
||||
resizeObserver.observe(webviewContainer);
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, [chatStore, projectStore, getSize]);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, [chatStore, projectStore, getSize]);
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 flex flex-row overflow-hidden pt-10 px-2 pb-2">
|
||||
<ReactFlowProvider>
|
||||
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center bg-surface-secondary border-solid border-border-tertiary rounded-2xl gap-2 relative overflow-hidden">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={30} minSize={20}>
|
||||
<ChatBox />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle={true} className="custom-resizable-handle" />
|
||||
<ResizablePanel>
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.activeWorkSpace && (
|
||||
<div className="w-full h-full flex-1 flex flex-col animate-in fade-in-0 pr-2 slide-in-from-right-2 duration-300">
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === "browser_agent" && (
|
||||
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<BrowserAgentWorkSpace />
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.activeWorkSpace === "workflow" && (
|
||||
<div className="w-full h-full flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<div className="w-full h-full flex flex-col rounded-2xl border border-transparent border-solid p-2 relative">
|
||||
{/*filter blur */}
|
||||
<div className="absolute inset-0 pointer-events-none bg-transparent rounded-xl"></div>
|
||||
<div className="w-full h-full relative z-10">
|
||||
<Workflow
|
||||
taskAssigning={
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.taskAssigning || []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === "developer_agent" && (
|
||||
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<TerminalAgentWrokSpace></TerminalAgentWrokSpace>
|
||||
{/* <Terminal content={[]} /> */}
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace === "documentWorkSpace" && (
|
||||
<div className="w-full h-[calc(100vh-104px)] flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<div className="w-full h-[calc(100vh-104px)] flex flex-col rounded-2xl border border-border-subtle-strong border-solid relative">
|
||||
{/*filter blur */}
|
||||
<div className="absolute inset-0 blur-bg pointer-events-none bg-white-50 rounded-xl"></div>
|
||||
<div className="w-full h-full relative z-10">
|
||||
<Folder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === "document_agent" && (
|
||||
<div className="w-full h-[calc(100vh-104px)] flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
|
||||
<div className="w-full h-[calc(100vh-104px)] flex flex-col rounded-2xl border border-border-subtle-strong border-solid relative">
|
||||
{/*filter blur */}
|
||||
<div className="absolute inset-0 blur-bg pointer-events-none bg-white-50 rounded-xl"></div>
|
||||
<div className="w-full h-full relative z-10">
|
||||
<Folder
|
||||
data={chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<BottomBar />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
{/* Fixed sidebar on the right
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-row overflow-hidden px-2 pb-2 pt-10">
|
||||
<ReactFlowProvider>
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 items-center justify-center gap-2 overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={30} minSize={20}>
|
||||
<ChatBox />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
withHandle={true}
|
||||
className="custom-resizable-handle"
|
||||
/>
|
||||
<ResizablePanel>
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.activeWorkSpace && (
|
||||
<div className="flex h-full w-full flex-1 flex-col pr-2 duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === 'browser_agent' && (
|
||||
<div className="flex h-[calc(100vh-104px)] w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<BrowserAgentWorkSpace />
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.activeWorkSpace === 'workflow' && (
|
||||
<div className="flex h-full w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<div className="relative flex h-full w-full flex-col rounded-2xl border border-solid border-transparent p-2">
|
||||
{/*filter blur */}
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Workflow
|
||||
taskAssigning={
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
?.taskAssigning || []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === 'developer_agent' && (
|
||||
<div className="flex h-[calc(100vh-104px)] w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<TerminalAgentWrokSpace></TerminalAgentWrokSpace>
|
||||
{/* <Terminal content={[]} /> */}
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace === 'documentWorkSpace' && (
|
||||
<div className="flex h-[calc(100vh-104px)] w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<div className="relative flex h-[calc(100vh-104px)] w-full flex-col rounded-2xl border border-solid border-border-subtle-strong">
|
||||
{/*filter blur */}
|
||||
<div className="blur-bg bg-white-50 pointer-events-none absolute inset-0 rounded-xl"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Folder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[chatStore.activeTaskId as string]
|
||||
.activeWorkSpace
|
||||
)?.type === 'document_agent' && (
|
||||
<div className="flex h-[calc(100vh-104px)] w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
|
||||
<div className="relative flex h-[calc(100vh-104px)] w-full flex-col rounded-2xl border border-solid border-border-subtle-strong">
|
||||
{/*filter blur */}
|
||||
<div className="blur-bg bg-white-50 pointer-events-none absolute inset-0 rounded-xl"></div>
|
||||
<div className="relative z-10 h-full w-full">
|
||||
<Folder
|
||||
data={chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
]?.taskAssigning?.find(
|
||||
(agent) =>
|
||||
agent.agent_id ===
|
||||
chatStore.tasks[
|
||||
chatStore.activeTaskId as string
|
||||
].activeWorkSpace
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<BottomBar />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
{/* Fixed sidebar on the right
|
||||
<div className="h-full z-30">
|
||||
<SideBar />
|
||||
</div>*/}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
<UpdateElectron />
|
||||
</div>
|
||||
);
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
<UpdateElectron />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import loginGif from '@/assets/login.gif';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useStackApp } from '@stackframe/react';
|
||||
|
|
@ -30,6 +29,9 @@ import WindowControls from '@/components/WindowControls';
|
|||
import { hasStackKeys } from '@/lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import background from '@/assets/background.png';
|
||||
import eigentLogo from '@/assets/logo/eigent_icon.png';
|
||||
|
||||
const HAS_STACK_KEYS = hasStackKeys();
|
||||
let lock = false;
|
||||
export default function Login() {
|
||||
|
|
@ -336,20 +338,6 @@ export default function Login() {
|
|||
ref={titlebarRef}
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
{/* Left spacer for macOS */}
|
||||
<div
|
||||
className={`${
|
||||
platform === 'darwin' ? 'w-[70px]' : 'w-0'
|
||||
} flex items-center justify-center`}
|
||||
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
|
||||
>
|
||||
{platform === 'darwin' && (
|
||||
<span className="text-label-md font-bold text-text-heading">
|
||||
Eigent
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Center drag region */}
|
||||
<div
|
||||
className="flex h-full flex-1 items-center"
|
||||
|
|
@ -374,12 +362,22 @@ export default function Login() {
|
|||
</div>
|
||||
|
||||
{/* Main content - image extends to top, form has padding */}
|
||||
<div className={`flex h-full items-center justify-center gap-2 p-2`}>
|
||||
<div className="flex h-full items-center justify-center rounded-3xl bg-white-100%">
|
||||
<img src={loginGif} className="h-full rounded-3xl object-cover" />
|
||||
</div>
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center pt-11">
|
||||
<div className="flex w-80 flex-1 flex-col items-center justify-center">
|
||||
<div
|
||||
className={`flex h-full items-center justify-center gap-2 px-2 pb-2 pt-10`}
|
||||
>
|
||||
<div
|
||||
className="flex h-full min-h-0 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary px-2 pb-2"
|
||||
style={{
|
||||
backgroundImage: `url(${background})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
|
||||
<img
|
||||
src={eigentLogo}
|
||||
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
|
||||
/>
|
||||
<div className="mb-4 flex items-end justify-between self-stretch">
|
||||
<div className="text-heading-lg font-bold text-text-heading">
|
||||
{t('layout.login')}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function Setting() {
|
|||
const version = useAppVersion();
|
||||
const { appearance } = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
const logoSrc = appearance === 'dark' ? logoWhite : logoBlack;
|
||||
const _logoSrc = appearance === 'dark' ? logoWhite : logoBlack;
|
||||
// Setting menu configuration
|
||||
const settingMenus = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,113 +12,131 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Circle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { proxyFetchGet, proxyFetchPost } from "@/api/http";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { proxyFetchGet, proxyFetchPost } from '@/api/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Circle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ConfigItem {
|
||||
name: string;
|
||||
env_vars: string[];
|
||||
name: string;
|
||||
env_vars: string[];
|
||||
}
|
||||
|
||||
export default function SettingAPI() {
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<ConfigItem[]>([]);
|
||||
const [envValues, setEnvValues] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<ConfigItem[]>([]);
|
||||
const [envValues, setEnvValues] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
proxyFetchGet("/api/config/info").then((res) => {
|
||||
const configs = Object.entries(res || {})
|
||||
.map(([name, v]: [string, any]) => ({ name, env_vars: v.env_vars }))
|
||||
.filter((item) => Array.isArray(item.env_vars) && item.env_vars.length > 0);
|
||||
setItems(configs);
|
||||
});
|
||||
proxyFetchGet("/api/configs").then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
const envMap: Record<string, string> = {};
|
||||
res.forEach((item: any) => {
|
||||
if (item.config_name && item.config_value) {
|
||||
envMap[item.config_name] = item.config_value;
|
||||
}
|
||||
});
|
||||
setEnvValues(envMap);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/config/info').then((res) => {
|
||||
const configs = Object.entries(res || {})
|
||||
.map(([name, v]: [string, any]) => ({ name, env_vars: v.env_vars }))
|
||||
.filter(
|
||||
(item) => Array.isArray(item.env_vars) && item.env_vars.length > 0
|
||||
);
|
||||
setItems(configs);
|
||||
});
|
||||
proxyFetchGet('/api/configs').then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
const envMap: Record<string, string> = {};
|
||||
res.forEach((item: any) => {
|
||||
if (item.config_name && item.config_value) {
|
||||
envMap[item.config_name] = item.config_value;
|
||||
}
|
||||
});
|
||||
setEnvValues(envMap);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (env: string, value: string) => {
|
||||
setEnvValues((prev) => ({ ...prev, [env]: value }));
|
||||
};
|
||||
const handleInputChange = (env: string, value: string) => {
|
||||
setEnvValues((prev) => ({ ...prev, [env]: value }));
|
||||
};
|
||||
|
||||
const handleVerify = async (configGroup: string, env: string) => {
|
||||
const value = envValues[env] || "";
|
||||
if (!value.trim()) {
|
||||
setErrors((prev) => ({ ...prev, [env]: t("layout.env-should-not-empty") }));
|
||||
return;
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, [env]: "" }));
|
||||
}
|
||||
setLoading((prev) => ({ ...prev, [env]: true }));
|
||||
try {
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_name: env,
|
||||
config_value: value,
|
||||
config_group: configGroup,
|
||||
});
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [env]: false }));
|
||||
}
|
||||
};
|
||||
const handleVerify = async (configGroup: string, env: string) => {
|
||||
const value = envValues[env] || '';
|
||||
if (!value.trim()) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[env]: t('layout.env-should-not-empty'),
|
||||
}));
|
||||
return;
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, [env]: '' }));
|
||||
}
|
||||
setLoading((prev) => ({ ...prev, [env]: true }));
|
||||
try {
|
||||
await proxyFetchPost('/api/configs', {
|
||||
config_name: env,
|
||||
config_value: value,
|
||||
config_group: configGroup,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to verify config:', e);
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [env]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{items.map((item) => (
|
||||
<div key={item.name} className="px-6 py-4 bg-bg-surface-tertiary rounded-2xl">
|
||||
<div>
|
||||
<div className="text-base font-bold leading-12 text-text-primary">{item.name}</div>
|
||||
</div>
|
||||
<div className="mt-md">
|
||||
<div>
|
||||
{item.env_vars.map((env) => (
|
||||
<div key={env} className="mt-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id={env}
|
||||
placeholder={env}
|
||||
className="w-full"
|
||||
value={envValues[env] || ""}
|
||||
onChange={(e) => {
|
||||
handleInputChange(env, e.target.value);
|
||||
if (errors[env]) setErrors((prev) => ({ ...prev, [env]: "" }));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="shadow-none px-sm py-xs bg-bg-fill-disabled"
|
||||
onClick={() => handleVerify(item.name, env)}
|
||||
disabled={loading[env]}
|
||||
>
|
||||
<span className="text-sm leading-13 text-text-inverse-primary">
|
||||
{loading[env] ? t("layout.loading") : t("layout.verify")}
|
||||
</span>
|
||||
<Circle className="w-4 h-4 text-icon-inverse-primary" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs leading-17 text-text-secondary mt-1.5">{env}</div>
|
||||
{errors[env] && (
|
||||
<span className="text-xs text-text-error mt-1">{errors[env]}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-bg-surface-tertiary rounded-2xl px-6 py-4"
|
||||
>
|
||||
<div>
|
||||
<div className="text-base font-bold leading-12 text-text-primary">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-md">
|
||||
<div>
|
||||
{item.env_vars.map((env) => (
|
||||
<div key={env} className="mt-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id={env}
|
||||
placeholder={env}
|
||||
className="w-full"
|
||||
value={envValues[env] || ''}
|
||||
onChange={(e) => {
|
||||
handleInputChange(env, e.target.value);
|
||||
if (errors[env])
|
||||
setErrors((prev) => ({ ...prev, [env]: '' }));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="bg-bg-fill-disabled px-sm py-xs shadow-none"
|
||||
onClick={() => handleVerify(item.name, env)}
|
||||
disabled={loading[env]}
|
||||
>
|
||||
<span className="text-sm leading-13 text-text-inverse-primary">
|
||||
{loading[env]
|
||||
? t('layout.loading')
|
||||
: t('layout.verify')}
|
||||
</span>
|
||||
<Circle className="text-icon-inverse-primary h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-1.5 text-xs leading-17 text-text-secondary">
|
||||
{env}
|
||||
</div>
|
||||
{errors[env] && (
|
||||
<span className="mt-1 text-xs text-text-error">
|
||||
{errors[env]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12,415 +12,415 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipSimple,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MCPEnvDialog } from "./components/MCPEnvDialog";
|
||||
import { Plus, Store, CircleAlert, ArrowLeft, ChevronLeft } from "lucide-react";
|
||||
import { proxyFetchDelete, proxyFetchGet, proxyFetchPost } from "@/api/http";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import githubIcon from "@/assets/github.svg";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import SearchInput from "@/components/SearchInput";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { proxyFetchDelete, proxyFetchGet, proxyFetchPost } from '@/api/http';
|
||||
import githubIcon from '@/assets/github.svg';
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { TooltipSimple } from '@/components/ui/tooltip';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { ChevronLeft, CircleAlert, Store } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MCPEnvDialog } from './components/MCPEnvDialog';
|
||||
interface MCPItem {
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
description: string;
|
||||
status: number | string;
|
||||
category?: { name: string };
|
||||
home_page?: string;
|
||||
install_command?: {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
homepage?: string;
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
description: string;
|
||||
status: number | string;
|
||||
category?: { name: string };
|
||||
home_page?: string;
|
||||
install_command?: {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
homepage?: string;
|
||||
}
|
||||
interface EnvValue {
|
||||
value: string;
|
||||
required: boolean;
|
||||
tip: string;
|
||||
interface _EnvValue {
|
||||
value: string;
|
||||
required: boolean;
|
||||
tip: string;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const STICKY_Z = 20;
|
||||
const _PAGE_SIZE = 10;
|
||||
const _STICKY_Z = 20;
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
}
|
||||
|
||||
// map category name to svg file name
|
||||
const categoryIconMap: Record<string, string> = {
|
||||
anthropic: "Anthropic",
|
||||
community: "Community",
|
||||
official: "Official",
|
||||
camel: "Camel",
|
||||
anthropic: 'Anthropic',
|
||||
community: 'Community',
|
||||
official: 'Official',
|
||||
camel: 'Camel',
|
||||
};
|
||||
|
||||
// load all svg files dynamically
|
||||
const svgIcons = import.meta.glob("@/assets/mcp/*.svg", {
|
||||
eager: true,
|
||||
query: "?url",
|
||||
import: "default",
|
||||
const svgIcons = import.meta.glob('@/assets/mcp/*.svg', {
|
||||
eager: true,
|
||||
query: '?url',
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
type MCPMarketProps = {
|
||||
onBack?: () => void;
|
||||
keyword?: string;
|
||||
onBack?: () => void;
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
export default function MCPMarket({ onBack, keyword: externalKeyword }: MCPMarketProps) {
|
||||
const { t } = useTranslation();
|
||||
const { checkAgentTool } = useAuthStore();
|
||||
const [items, setItems] = useState<MCPItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const effectiveKeyword = externalKeyword !== undefined ? externalKeyword : keyword;
|
||||
const debouncedKeyword = useDebounce(effectiveKeyword, 400);
|
||||
const loader = useRef<HTMLDivElement | null>(null);
|
||||
const [installing, setInstalling] = useState<{ [id: number]: boolean }>({});
|
||||
const [installed, setInstalled] = useState<{ [id: number]: boolean }>({});
|
||||
const [installedIds, setInstalledIds] = useState<number[]>([]);
|
||||
const [mcpCategory, setMcpCategory] = useState<
|
||||
{ id: number; name: string }[]
|
||||
>([]);
|
||||
export default function MCPMarket({
|
||||
onBack,
|
||||
keyword: externalKeyword,
|
||||
}: MCPMarketProps) {
|
||||
const { t } = useTranslation();
|
||||
const { checkAgentTool } = useAuthStore();
|
||||
const [items, setItems] = useState<MCPItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const effectiveKeyword =
|
||||
externalKeyword !== undefined ? externalKeyword : keyword;
|
||||
const debouncedKeyword = useDebounce(effectiveKeyword, 400);
|
||||
const loader = useRef<HTMLDivElement | null>(null);
|
||||
const [installing, setInstalling] = useState<{ [id: number]: boolean }>({});
|
||||
const [installed, setInstalled] = useState<{ [id: number]: boolean }>({});
|
||||
const [installedIds, setInstalledIds] = useState<number[]>([]);
|
||||
const [mcpCategory, setMcpCategory] = useState<
|
||||
{ id: number; name: string }[]
|
||||
>([]);
|
||||
|
||||
// environment variable configuration
|
||||
const [showEnvConfig, setShowEnvConfig] = useState(false);
|
||||
const [activeMcp, setActiveMcp] = useState<MCPItem | null>(null);
|
||||
// environment variable configuration
|
||||
const [showEnvConfig, setShowEnvConfig] = useState(false);
|
||||
const [activeMcp, setActiveMcp] = useState<MCPItem | null>(null);
|
||||
|
||||
const [categoryId, setCategoryId] = useState<number | undefined>(undefined);
|
||||
const effectiveCategoryId = categoryId;
|
||||
const [userInstallMcp, setUserInstallMcp] = useState<any | undefined>([]);
|
||||
// get installed MCP list
|
||||
useEffect(() => {
|
||||
proxyFetchGet("/api/mcp/users").then((res) => {
|
||||
let ids: number[] = [];
|
||||
if (Array.isArray(res)) {
|
||||
setUserInstallMcp(res);
|
||||
ids = res.map((item: any) => item.mcp_id);
|
||||
} else if (Array.isArray(res.items)) {
|
||||
setUserInstallMcp(res.items);
|
||||
ids = res.items.map((item: any) => item.mcp_id);
|
||||
}
|
||||
setInstalledIds(ids);
|
||||
});
|
||||
}, []);
|
||||
const [categoryId, setCategoryId] = useState<number | undefined>(undefined);
|
||||
const effectiveCategoryId = categoryId;
|
||||
const [userInstallMcp, setUserInstallMcp] = useState<any | undefined>([]);
|
||||
// get installed MCP list
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/mcp/users').then((res) => {
|
||||
let ids: number[] = [];
|
||||
if (Array.isArray(res)) {
|
||||
setUserInstallMcp(res);
|
||||
ids = res.map((item: any) => item.mcp_id);
|
||||
} else if (Array.isArray(res.items)) {
|
||||
setUserInstallMcp(res.items);
|
||||
ids = res.items.map((item: any) => item.mcp_id);
|
||||
}
|
||||
setInstalledIds(ids);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// get MCP categories
|
||||
useEffect(() => {
|
||||
proxyFetchGet("/api/mcp/categories").then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
setMcpCategory(res);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
// get MCP categories
|
||||
useEffect(() => {
|
||||
proxyFetchGet('/api/mcp/categories').then((res) => {
|
||||
if (Array.isArray(res)) {
|
||||
setMcpCategory(res);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// load data
|
||||
const loadData = useCallback(
|
||||
async (pageNum: number, kw: string, catId?: number, pageSize = 20) => {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const params: any = { page: pageNum, size: pageSize, keyword: kw };
|
||||
if (catId) params.category_id = catId;
|
||||
const res = await proxyFetchGet("/api/mcps", params);
|
||||
if (res && Array.isArray(res.items)) {
|
||||
// frontend deduplication
|
||||
const all: MCPItem[] =
|
||||
pageNum === 1 ? res.items : [...items, ...res.items];
|
||||
const unique: MCPItem[] = Array.from(
|
||||
new Map(all.map((i: MCPItem) => [i.id, i])).values()
|
||||
);
|
||||
setItems(unique);
|
||||
setHasMore(res.items.length === pageSize);
|
||||
} else {
|
||||
if (pageNum === 1) setItems([]);
|
||||
setHasMore(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "Load failed");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[items]
|
||||
);
|
||||
// load data
|
||||
const loadData = useCallback(
|
||||
async (pageNum: number, kw: string, catId?: number, pageSize = 20) => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const params: any = { page: pageNum, size: pageSize, keyword: kw };
|
||||
if (catId) params.category_id = catId;
|
||||
const res = await proxyFetchGet('/api/mcps', params);
|
||||
if (res && Array.isArray(res.items)) {
|
||||
// frontend deduplication
|
||||
const all: MCPItem[] =
|
||||
pageNum === 1 ? res.items : [...items, ...res.items];
|
||||
const unique: MCPItem[] = Array.from(
|
||||
new Map(all.map((i: MCPItem) => [i.id, i])).values()
|
||||
);
|
||||
setItems(unique);
|
||||
setHasMore(res.items.length === pageSize);
|
||||
} else {
|
||||
if (pageNum === 1) setItems([]);
|
||||
setHasMore(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Load failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
loadData(1, debouncedKeyword, effectiveCategoryId);
|
||||
// eslint-disable-next-line
|
||||
}, [debouncedKeyword, effectiveCategoryId]);
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
loadData(1, debouncedKeyword, effectiveCategoryId);
|
||||
// eslint-disable-next-line
|
||||
}, [debouncedKeyword, effectiveCategoryId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page > 1) loadData(page, debouncedKeyword, effectiveCategoryId);
|
||||
// eslint-disable-next-line
|
||||
}, [page]);
|
||||
useEffect(() => {
|
||||
if (page > 1) loadData(page, debouncedKeyword, effectiveCategoryId);
|
||||
// eslint-disable-next-line
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMore || isLoading) return;
|
||||
const node = loader.current;
|
||||
if (!node) return;
|
||||
const observer = new window.IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setPage((p) => (isLoading || !hasMore ? p : p + 1));
|
||||
}
|
||||
},
|
||||
{ root: null, rootMargin: "0px", threshold: 0.1 }
|
||||
);
|
||||
observer.observe(node);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [hasMore, isLoading]);
|
||||
useEffect(() => {
|
||||
if (!hasMore || isLoading) return;
|
||||
const node = loader.current;
|
||||
if (!node) return;
|
||||
const observer = new window.IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setPage((p) => (isLoading || !hasMore ? p : p + 1));
|
||||
}
|
||||
},
|
||||
{ root: null, rootMargin: '0px', threshold: 0.1 }
|
||||
);
|
||||
observer.observe(node);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [hasMore, isLoading]);
|
||||
|
||||
const checkEnv = (id: number) => {
|
||||
const mcp = items.find((mcp) => mcp.id === id);
|
||||
if (mcp && Object.keys(mcp?.install_command?.env || {}).length > 0) {
|
||||
setActiveMcp(mcp);
|
||||
setShowEnvConfig(true);
|
||||
} else {
|
||||
installMcp(id);
|
||||
}
|
||||
};
|
||||
const onConnect = (mcp: MCPItem) => {
|
||||
console.log(mcp);
|
||||
setItems((prev) =>
|
||||
prev.map((item) => (item.id === mcp.id ? { ...item, ...mcp } : item))
|
||||
);
|
||||
installMcp(mcp.id);
|
||||
onClose();
|
||||
};
|
||||
const onClose = () => {
|
||||
setShowEnvConfig(false);
|
||||
setActiveMcp(null);
|
||||
};
|
||||
const installMcp = async (id: number) => {
|
||||
setInstalling((prev) => ({ ...prev, [id]: true }));
|
||||
try {
|
||||
const mcpItem = items.find((item) => item.id === id);
|
||||
const res = await proxyFetchPost("/api/mcp/install?mcp_id=" + id);
|
||||
if (res) {
|
||||
console.log(res);
|
||||
setUserInstallMcp((prev: any) => [...prev, res]);
|
||||
}
|
||||
setInstalled((prev) => ({ ...prev, [id]: true }));
|
||||
setInstalledIds((prev) => [...prev, id]);
|
||||
// notify main process
|
||||
if (window.ipcRenderer && mcpItem) {
|
||||
await window.ipcRenderer.invoke(
|
||||
"mcp-install",
|
||||
mcpItem.key,
|
||||
mcpItem.install_command
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setInstalling((prev) => ({ ...prev, [id]: false }));
|
||||
}
|
||||
};
|
||||
const checkEnv = (id: number) => {
|
||||
const mcp = items.find((mcp) => mcp.id === id);
|
||||
if (mcp && Object.keys(mcp?.install_command?.env || {}).length > 0) {
|
||||
setActiveMcp(mcp);
|
||||
setShowEnvConfig(true);
|
||||
} else {
|
||||
installMcp(id);
|
||||
}
|
||||
};
|
||||
const onConnect = (mcp: MCPItem) => {
|
||||
console.log(mcp);
|
||||
setItems((prev) =>
|
||||
prev.map((item) => (item.id === mcp.id ? { ...item, ...mcp } : item))
|
||||
);
|
||||
installMcp(mcp.id);
|
||||
onClose();
|
||||
};
|
||||
const onClose = () => {
|
||||
setShowEnvConfig(false);
|
||||
setActiveMcp(null);
|
||||
};
|
||||
const installMcp = async (id: number) => {
|
||||
setInstalling((prev) => ({ ...prev, [id]: true }));
|
||||
try {
|
||||
const mcpItem = items.find((item) => item.id === id);
|
||||
const res = await proxyFetchPost('/api/mcp/install?mcp_id=' + id);
|
||||
if (res) {
|
||||
console.log(res);
|
||||
setUserInstallMcp((prev: any) => [...prev, res]);
|
||||
}
|
||||
setInstalled((prev) => ({ ...prev, [id]: true }));
|
||||
setInstalledIds((prev) => [...prev, id]);
|
||||
// notify main process
|
||||
if (window.ipcRenderer && mcpItem) {
|
||||
await window.ipcRenderer.invoke(
|
||||
'mcp-install',
|
||||
mcpItem.key,
|
||||
mcpItem.install_command
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error installing MCP:', e);
|
||||
} finally {
|
||||
setInstalling((prev) => ({ ...prev, [id]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) onBack();
|
||||
else window.history.back();
|
||||
};
|
||||
const handleBack = () => {
|
||||
if (onBack) onBack();
|
||||
else window.history.back();
|
||||
};
|
||||
|
||||
const handleDelete = async (deleteTarget: MCPItem) => {
|
||||
if (!deleteTarget) return;
|
||||
try {
|
||||
checkAgentTool(deleteTarget.name);
|
||||
console.log(userInstallMcp, deleteTarget);
|
||||
const id = userInstallMcp.find(
|
||||
(item: any) => item.mcp_id === deleteTarget.id
|
||||
)?.id;
|
||||
console.log("deleteTarget", deleteTarget);
|
||||
await proxyFetchDelete(`/api/mcp/users/${id}`);
|
||||
// notify main process
|
||||
if (window.ipcRenderer) {
|
||||
await window.ipcRenderer.invoke("mcp-remove", deleteTarget.key);
|
||||
}
|
||||
setInstalledIds((prev) =>
|
||||
prev.filter((item) => item !== deleteTarget.id)
|
||||
);
|
||||
setInstalled((prev) => ({ ...prev, [deleteTarget.id]: false }));
|
||||
loadData(1, debouncedKeyword, categoryId, page * 20);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center ">
|
||||
{externalKeyword === undefined && (
|
||||
<>
|
||||
<div className="text-body flex items-center justify-between sticky top-0 z-[20] py-2 mb-0 w-full max-w-4xl">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="mr-2"
|
||||
>
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</Button>
|
||||
<span className="text-base font-bold leading-12 text-text-primary">
|
||||
{t("setting.mcp-market")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-40 max-w-4xl">
|
||||
<SearchInput
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
const handleDelete = async (deleteTarget: MCPItem) => {
|
||||
if (!deleteTarget) return;
|
||||
try {
|
||||
checkAgentTool(deleteTarget.name);
|
||||
console.log(userInstallMcp, deleteTarget);
|
||||
const id = userInstallMcp.find(
|
||||
(item: any) => item.mcp_id === deleteTarget.id
|
||||
)?.id;
|
||||
console.log('deleteTarget', deleteTarget);
|
||||
await proxyFetchDelete(`/api/mcp/users/${id}`);
|
||||
// notify main process
|
||||
if (window.ipcRenderer) {
|
||||
await window.ipcRenderer.invoke('mcp-remove', deleteTarget.key);
|
||||
}
|
||||
setInstalledIds((prev) =>
|
||||
prev.filter((item) => item !== deleteTarget.id)
|
||||
);
|
||||
setInstalled((prev) => ({ ...prev, [deleteTarget.id]: false }));
|
||||
loadData(1, debouncedKeyword, categoryId, page * 20);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center">
|
||||
{externalKeyword === undefined && (
|
||||
<>
|
||||
<div className="text-body sticky top-0 z-[20] mb-0 flex w-full max-w-4xl items-center justify-between py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="mr-2"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<span className="text-base font-bold leading-12 text-text-primary">
|
||||
{t('setting.mcp-market')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-40 max-w-4xl">
|
||||
<SearchInput
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Category toggle row */}
|
||||
<div className="w-full flex py-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={categoryId ? String(categoryId) : "all"}
|
||||
onValueChange={(val) => setCategoryId(!val || val === "all" ? undefined : Number(val))}
|
||||
className="flex flex-wrap"
|
||||
>
|
||||
<ToggleGroupItem value="all">
|
||||
{t("setting.all")}
|
||||
</ToggleGroupItem>
|
||||
{mcpCategory.map((cat) => (
|
||||
<ToggleGroupItem key={cat.id} value={String(cat.id)}>
|
||||
{cat.name}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{/* Category toggle row */}
|
||||
<div className="flex w-full py-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={categoryId ? String(categoryId) : 'all'}
|
||||
onValueChange={(val) =>
|
||||
setCategoryId(!val || val === 'all' ? undefined : Number(val))
|
||||
}
|
||||
className="flex flex-wrap"
|
||||
>
|
||||
<ToggleGroupItem value="all">{t('setting.all')}</ToggleGroupItem>
|
||||
{mcpCategory.map((cat) => (
|
||||
<ToggleGroupItem key={cat.id} value={String(cat.id)}>
|
||||
{cat.name}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* list */}
|
||||
<MCPEnvDialog
|
||||
showEnvConfig={showEnvConfig}
|
||||
onClose={onClose}
|
||||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
<div className="flex flex-col gap-4 w-full pt-4">
|
||||
{isLoading && items.length === 0 && (
|
||||
<div className="text-center py-8 text-text-muted">{t("setting.loading")}</div>
|
||||
)}
|
||||
{error && <div className="text-center py-8 text-text-error">{error}</div>}
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="text-center py-8 text-text-muted">{t("setting.no-mcp-services")}</div>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-4 bg-surface-secondary rounded-2xl flex items-center"
|
||||
>
|
||||
{/* Left: Icon */}
|
||||
<div className="flex items-center mr-4">
|
||||
{(() => {
|
||||
const catName = item.category?.name;
|
||||
const iconKey = catName ? categoryIconMap[catName] : undefined;
|
||||
const iconUrl = iconKey
|
||||
? (svgIcons[`/src/assets/mcp/${iconKey}.svg`] as string)
|
||||
: undefined;
|
||||
return iconUrl ? (
|
||||
<img src={iconUrl} alt={catName} className="w-9 h-11" />
|
||||
) : (
|
||||
<Store className="w-9 h-11 text-icon-primary" />
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-xs w-full pb-1">
|
||||
<div className="flex items-center gap-xs flex-1">
|
||||
<span className="text-base leading-9 font-bold text-text-primary truncate ">
|
||||
{item.name}
|
||||
</span>
|
||||
<TooltipSimple content={item.description}>
|
||||
<CircleAlert className="w-4 h-4 text-icon-secondary" />
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<Button
|
||||
variant={
|
||||
!installedIds.includes(item.id) ? "primary" : "secondary"
|
||||
}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
installedIds.includes(item.id)
|
||||
? handleDelete(item)
|
||||
: checkEnv(item.id)
|
||||
}
|
||||
>
|
||||
{installedIds.includes(item.id)
|
||||
? t("setting.uninstall")
|
||||
: installing[item.id]
|
||||
? t("setting.installing")
|
||||
: installed[item.id]
|
||||
? t("setting.uninstall")
|
||||
: t("setting.install")}
|
||||
</Button>
|
||||
</div>
|
||||
{item.home_page &&
|
||||
item.home_page.startsWith("https://github.com/") && (
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={githubIcon}
|
||||
alt="github"
|
||||
style={{
|
||||
width: 14.7,
|
||||
height: 14.7,
|
||||
marginRight: 4,
|
||||
display: "inline-block",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
<span className="self-stretch items-center justify-center text-xs font-medium leading-3">
|
||||
{(() => {
|
||||
const parts = item.home_page.split("/");
|
||||
return parts.length > 4 ? parts[4] : item.home_page;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-text-muted mt-1 break-words whitespace-pre-line">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={loader} />
|
||||
{isLoading && items.length > 0 && (
|
||||
<div className="text-center py-4 text-text-muted">{t("setting.loading-more")}</div>
|
||||
)}
|
||||
{!hasMore && items.length > 0 && (
|
||||
<div className="text-center py-4 text-text-muted">
|
||||
{t("setting.no-more-mcp-servers")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{/* list */}
|
||||
<MCPEnvDialog
|
||||
showEnvConfig={showEnvConfig}
|
||||
onClose={onClose}
|
||||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
<div className="flex w-full flex-col gap-4 pt-4">
|
||||
{isLoading && items.length === 0 && (
|
||||
<div className="py-8 text-center text-text-muted">
|
||||
{t('setting.loading')}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-text-error">{error}</div>
|
||||
)}
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<div className="py-8 text-center text-text-muted">
|
||||
{t('setting.no-mcp-services')}
|
||||
</div>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center rounded-2xl bg-surface-secondary p-4"
|
||||
>
|
||||
{/* Left: Icon */}
|
||||
<div className="mr-4 flex items-center">
|
||||
{(() => {
|
||||
const catName = item.category?.name;
|
||||
const iconKey = catName ? categoryIconMap[catName] : undefined;
|
||||
const iconUrl = iconKey
|
||||
? (svgIcons[`/src/assets/mcp/${iconKey}.svg`] as string)
|
||||
: undefined;
|
||||
return iconUrl ? (
|
||||
<img src={iconUrl} alt={catName} className="h-11 w-9" />
|
||||
) : (
|
||||
<Store className="h-11 w-9 text-icon-primary" />
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
||||
<div className="flex w-full items-center gap-xs pb-1">
|
||||
<div className="flex flex-1 items-center gap-xs">
|
||||
<span className="truncate text-base font-bold leading-9 text-text-primary">
|
||||
{item.name}
|
||||
</span>
|
||||
<TooltipSimple content={item.description}>
|
||||
<CircleAlert className="h-4 w-4 text-icon-secondary" />
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<Button
|
||||
variant={
|
||||
!installedIds.includes(item.id) ? 'primary' : 'secondary'
|
||||
}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
installedIds.includes(item.id)
|
||||
? handleDelete(item)
|
||||
: checkEnv(item.id)
|
||||
}
|
||||
>
|
||||
{installedIds.includes(item.id)
|
||||
? t('setting.uninstall')
|
||||
: installing[item.id]
|
||||
? t('setting.installing')
|
||||
: installed[item.id]
|
||||
? t('setting.uninstall')
|
||||
: t('setting.install')}
|
||||
</Button>
|
||||
</div>
|
||||
{item.home_page &&
|
||||
item.home_page.startsWith('https://github.com/') && (
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={githubIcon}
|
||||
alt="github"
|
||||
style={{
|
||||
width: 14.7,
|
||||
height: 14.7,
|
||||
marginRight: 4,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
<span className="items-center justify-center self-stretch text-xs font-medium leading-3">
|
||||
{(() => {
|
||||
const parts = item.home_page.split('/');
|
||||
return parts.length > 4 ? parts[4] : item.home_page;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 whitespace-pre-line break-words text-sm text-text-muted">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={loader} />
|
||||
{isLoading && items.length > 0 && (
|
||||
<div className="py-4 text-center text-text-muted">
|
||||
{t('setting.loading-more')}
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && items.length > 0 && (
|
||||
<div className="py-4 text-center text-text-muted">
|
||||
{t('setting.no-more-mcp-servers')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ type SidebarTab =
|
|||
export default function SettingModels() {
|
||||
const { modelType, cloud_model_type, setModelType, setCloudModelType } =
|
||||
useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const _navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const getValidateMessage = (res: any) =>
|
||||
res?.message ??
|
||||
|
|
@ -106,7 +106,7 @@ export default function SettingModels() {
|
|||
res?.detail?.error?.message ??
|
||||
res?.error?.message ??
|
||||
t('setting.validate-failed');
|
||||
const [items, setItems] = useState<Provider[]>(
|
||||
const [items, _setItems] = useState<Provider[]>(
|
||||
INIT_PROVODERS.filter((p) => p.id !== 'local')
|
||||
);
|
||||
const [form, setForm] = useState(() =>
|
||||
|
|
@ -139,7 +139,7 @@ export default function SettingModels() {
|
|||
apiHost: '',
|
||||
}))
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [_collapsed, _setCollapsed] = useState(false);
|
||||
|
||||
// Sidebar selected tab - default to cloud
|
||||
const [selectedTab, setSelectedTab] = useState<SidebarTab>('cloud');
|
||||
|
|
@ -266,6 +266,7 @@ export default function SettingModels() {
|
|||
setCloudPrefer(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching providers:', e);
|
||||
// ignore error
|
||||
}
|
||||
})();
|
||||
|
|
@ -274,7 +275,7 @@ export default function SettingModels() {
|
|||
fetchSubscription();
|
||||
updateCredits();
|
||||
}
|
||||
}, []);
|
||||
}, [items, modelType]);
|
||||
|
||||
// Get current default model display text
|
||||
const getDefaultModelDisplayText = (): string => {
|
||||
|
|
@ -760,6 +761,7 @@ export default function SettingModels() {
|
|||
setForm((f) => f.map((fi, i) => ({ ...fi, prefer: i === idx }))); // Only one prefer allowed
|
||||
setLocalPrefer(false);
|
||||
} catch (e) {
|
||||
console.error('Error switching model:', e);
|
||||
// Optional: add error message
|
||||
}
|
||||
};
|
||||
|
|
@ -793,6 +795,7 @@ export default function SettingModels() {
|
|||
setLocalPrefer(true);
|
||||
setCloudPrefer(false);
|
||||
} catch (e) {
|
||||
console.error('Error switching local model:', e);
|
||||
// Optional: add error message
|
||||
}
|
||||
};
|
||||
|
|
@ -814,6 +817,7 @@ export default function SettingModels() {
|
|||
setActiveModelIdx(null);
|
||||
toast.success(t('setting.reset-success'));
|
||||
} catch (e) {
|
||||
console.error('Error resetting local model:', e);
|
||||
toast.error(t('setting.reset-failed'));
|
||||
}
|
||||
};
|
||||
|
|
@ -852,6 +856,7 @@ export default function SettingModels() {
|
|||
}
|
||||
toast.success(t('setting.reset-success'));
|
||||
} catch (e) {
|
||||
console.error('Error deleting model:', e);
|
||||
toast.error(t('setting.reset-failed'));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,18 +17,21 @@ import { Button } from '@/components/ui/button';
|
|||
import { Switch } from '@/components/ui/switch';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export default function SettingPrivacy() {
|
||||
const { email } = useAuthStore();
|
||||
const [_privacy, setPrivacy] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const API_FIELDS = [
|
||||
'take_screenshot',
|
||||
'access_local_software',
|
||||
'access_your_address',
|
||||
'password_storage',
|
||||
];
|
||||
const API_FIELDS = useMemo(
|
||||
() => [
|
||||
'take_screenshot',
|
||||
'access_local_software',
|
||||
'access_your_address',
|
||||
'password_storage',
|
||||
],
|
||||
[]
|
||||
);
|
||||
const [settings, setSettings] = useState([
|
||||
{
|
||||
title: t('setting.allow-agent-to-take-screenshots'),
|
||||
|
|
@ -71,7 +74,7 @@ export default function SettingPrivacy() {
|
|||
setPrivacy(!hasFalse);
|
||||
})
|
||||
.catch((err) => console.error('Failed to fetch settings:', err));
|
||||
}, []);
|
||||
}, [API_FIELDS]);
|
||||
|
||||
const handleTurnOnAll = (type: boolean) => {
|
||||
const newSettings = settings.map((item) => ({
|
||||
|
|
@ -90,7 +93,7 @@ export default function SettingPrivacy() {
|
|||
proxyFetchPut('/api/user/privacy', requestData);
|
||||
};
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
const _handleToggle = (index: number) => {
|
||||
setSettings((prev) => {
|
||||
const newSettings = [...prev];
|
||||
newSettings[index] = {
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ export default function CookieManager() {
|
|||
<div className="rounded-2xl bg-surface-secondary px-6 py-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-text-primary flex items-center gap-2 text-base font-bold leading-12">
|
||||
<div className="flex items-center gap-2 text-base font-bold leading-12 text-text-primary">
|
||||
<Cookie className="h-5 w-5" />
|
||||
{t('setting.cookie-manager')}
|
||||
</div>
|
||||
<div className="text-text-secondary mt-1 text-sm leading-13">
|
||||
<div className="mt-1 text-sm leading-13 text-text-secondary">
|
||||
{t('setting.cookie-manager-description')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -153,7 +153,7 @@ export default function CookieManager() {
|
|||
|
||||
{/* Search Bar */}
|
||||
<div className="relative mb-4">
|
||||
<Search className="text-text-tertiary absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-text-tertiary" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('setting.search-domains')}
|
||||
|
|
@ -166,12 +166,12 @@ export default function CookieManager() {
|
|||
{/* Cookie List */}
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="text-text-secondary py-8 text-center">
|
||||
<div className="py-8 text-center text-text-secondary">
|
||||
<RefreshCw className="mx-auto mb-2 h-6 w-6 animate-spin" />
|
||||
{t('setting.loading-cookies')}
|
||||
</div>
|
||||
) : filteredDomains.length === 0 ? (
|
||||
<div className="text-text-secondary py-8 text-center">
|
||||
<div className="py-8 text-center text-text-secondary">
|
||||
<Cookie className="mx-auto mb-3 h-12 w-12 opacity-30" />
|
||||
<div className="mb-1 text-base font-medium">
|
||||
{domains.length === 0
|
||||
|
|
@ -191,10 +191,10 @@ export default function CookieManager() {
|
|||
className="flex items-center justify-between rounded-lg border border-border-primary bg-surface-primary p-3 transition-colors hover:border-border-secondary"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-text-primary truncate font-medium">
|
||||
<div className="truncate font-medium text-text-primary">
|
||||
{item.domain}
|
||||
</div>
|
||||
<div className="text-text-tertiary mt-1 flex items-center gap-3 text-xs">
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-text-tertiary">
|
||||
<span>
|
||||
{t('setting.cookies-count', { count: item.cookie_count })}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default function MCPListItem({
|
|||
<div className="mb-4 flex items-center justify-between gap-4 rounded-2xl bg-surface-secondary p-4">
|
||||
<div className="flex items-center gap-xs">
|
||||
<div className="mx-xs h-3 w-3 rounded-full bg-green-500"></div>
|
||||
<div className="text-text-primary text-base font-bold leading-9">
|
||||
<div className="text-base font-bold leading-9 text-text-primary">
|
||||
{item.mcp_name}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -12,20 +12,22 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import loginGif from '@/assets/login.gif';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useStackApp } from '@stackframe/react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
import { proxyFetchPost } from '@/api/http';
|
||||
import background from '@/assets/background.png';
|
||||
import eyeOff from '@/assets/eye-off.svg';
|
||||
import eye from '@/assets/eye.svg';
|
||||
import github2 from '@/assets/github2.svg';
|
||||
import google from '@/assets/google.svg';
|
||||
import eigentLogo from '@/assets/logo/eigent_icon.png';
|
||||
import WindowControls from '@/components/WindowControls';
|
||||
import { hasStackKeys } from '@/lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -52,6 +54,8 @@ export default function SignUp() {
|
|||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [generalError, setGeneralError] = useState('');
|
||||
const titlebarRef = useRef<HTMLDivElement | null>(null);
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
|
@ -262,134 +266,188 @@ export default function SignUp() {
|
|||
};
|
||||
}, [handleAuthCode]);
|
||||
|
||||
useEffect(() => {
|
||||
const p = window.electronAPI.getPlatform();
|
||||
setPlatform(p);
|
||||
|
||||
if (platform === 'darwin') {
|
||||
titlebarRef.current?.classList.add('mac');
|
||||
}
|
||||
}, [platform]);
|
||||
|
||||
return (
|
||||
<div className={`flex h-full items-center justify-center gap-2 p-2`}>
|
||||
<div className="flex h-[calc(800px-16px)] items-center justify-center rounded-3xl bg-white-100%">
|
||||
<img src={loginGif} className="h-full rounded-3xl object-cover" />
|
||||
<div className="relative flex h-full flex-col overflow-hidden">
|
||||
{/* Titlebar with drag region and window controls */}
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 z-50 flex !h-9 items-center justify-between py-1 pl-2"
|
||||
id="signup-titlebar"
|
||||
ref={titlebarRef}
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
{/* Center drag region */}
|
||||
<div
|
||||
className="flex h-full flex-1 items-center"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
<div className="h-10 flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Right window controls */}
|
||||
<div
|
||||
style={
|
||||
{
|
||||
WebkitAppRegion: 'no-drag',
|
||||
pointerEvents: 'auto',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<WindowControls />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center">
|
||||
<div className="flex w-80 flex-1 flex-col items-center justify-center">
|
||||
<div className="mb-4 flex items-end justify-between self-stretch">
|
||||
<div className="text-heading-lg font-bold text-text-heading">
|
||||
{t('layout.sign-up')}
|
||||
|
||||
{/* Main content - image extends to top, form has padding */}
|
||||
<div
|
||||
className={`flex h-full items-center justify-center gap-2 px-2 pb-2 pt-10`}
|
||||
>
|
||||
<div
|
||||
className="flex h-full min-h-0 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary px-2 pb-2"
|
||||
style={{
|
||||
backgroundImage: `url(${background})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
|
||||
<img
|
||||
src={eigentLogo}
|
||||
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
|
||||
/>
|
||||
<div className="mb-4 flex items-end justify-between self-stretch">
|
||||
<div className="text-heading-lg font-bold text-text-heading">
|
||||
{t('layout.sign-up')}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
{t('layout.login')}
|
||||
</Button>
|
||||
</div>
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="w-full pt-6">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('google')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={google} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-google-sign-up')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('github')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={github2} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-github-sign-up')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="mb-6 mt-2 w-full text-center font-inter text-[15px] font-medium leading-[22px] text-[#222]">
|
||||
{t('layout.or')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{generalError && (
|
||||
<p className="mb-4 mt-1 text-label-md text-text-cuation">
|
||||
{generalError}
|
||||
</p>
|
||||
)}
|
||||
<div className="relative mb-4 flex w-full flex-col gap-4">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
size="default"
|
||||
title={t('layout.email')}
|
||||
placeholder={t('layout.enter-your-email')}
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
state={errors.email ? 'error' : undefined}
|
||||
note={errors.email}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
title={t('layout.password')}
|
||||
size="default"
|
||||
type={hidePassword ? 'password' : 'text'}
|
||||
required
|
||||
placeholder={t('layout.enter-your-password')}
|
||||
value={formData.password}
|
||||
onChange={(e) =>
|
||||
handleInputChange('password', e.target.value)
|
||||
}
|
||||
state={errors.password ? 'error' : undefined}
|
||||
note={errors.password}
|
||||
backIcon={<img src={hidePassword ? eye : eyeOff} />}
|
||||
onBackIconClick={() => setHidePassword(!hidePassword)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="invite_code"
|
||||
title={t('layout.invitation-code-optional')}
|
||||
size="default"
|
||||
type="text"
|
||||
placeholder={t('layout.enter-your-invite-code')}
|
||||
value={formData.invite_code}
|
||||
onChange={(e) =>
|
||||
handleInputChange('invite_code', e.target.value)
|
||||
}
|
||||
state={errors.invite_code ? 'error' : undefined}
|
||||
note={errors.invite_code}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/login')}
|
||||
onClick={handleRegister}
|
||||
size="md"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="w-full rounded-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('layout.login')}
|
||||
<span className="flex-1">
|
||||
{isLoading ? t('layout.signing-up') : t('layout.sign-up')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="w-full pt-6">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('google')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={google} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-google-sign-up')}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => handleReloadBtn('github')}
|
||||
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img src={github2} className="h-5 w-5" />
|
||||
<span className="ml-2">
|
||||
{t('layout.continue-with-github-sign-up')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{HAS_STACK_KEYS && (
|
||||
<div className="mb-6 mt-2 w-full text-center font-inter text-[15px] font-medium leading-[22px] text-[#222]">
|
||||
{t('layout.or')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{generalError && (
|
||||
<p className="mb-4 mt-1 text-label-md text-text-cuation">
|
||||
{generalError}
|
||||
</p>
|
||||
)}
|
||||
<div className="relative mb-4 flex w-full flex-col gap-4">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
size="default"
|
||||
title={t('layout.email')}
|
||||
placeholder={t('layout.enter-your-email')}
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
state={errors.email ? 'error' : undefined}
|
||||
note={errors.email}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
title={t('layout.password')}
|
||||
size="default"
|
||||
type={hidePassword ? 'password' : 'text'}
|
||||
required
|
||||
placeholder={t('layout.enter-your-password')}
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
state={errors.password ? 'error' : undefined}
|
||||
note={errors.password}
|
||||
backIcon={<img src={hidePassword ? eye : eyeOff} />}
|
||||
onBackIconClick={() => setHidePassword(!hidePassword)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="invite_code"
|
||||
title={t('layout.invitation-code-optional')}
|
||||
size="default"
|
||||
type="text"
|
||||
placeholder={t('layout.enter-your-invite-code')}
|
||||
value={formData.invite_code}
|
||||
onChange={(e) =>
|
||||
handleInputChange('invite_code', e.target.value)
|
||||
}
|
||||
state={errors.invite_code ? 'error' : undefined}
|
||||
note={errors.invite_code}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleRegister}
|
||||
size="md"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="w-full rounded-full"
|
||||
disabled={isLoading}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://www.eigent.ai/privacy-policy',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="flex-1">
|
||||
{isLoading ? t('layout.signing-up') : t('layout.sign-up')}
|
||||
</span>
|
||||
{t('layout.privacy-policy')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://www.eigent.ai/privacy-policy',
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('layout.privacy-policy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { generateUniqueId } from '@/lib';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { create } from 'zustand';
|
||||
import { createChatStoreInstance, VanillaChatStore } from './chatStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
|
||||
export enum ProjectType {
|
||||
NORMAL = 'normal',
|
||||
|
|
|
|||
|
|
@ -555,3 +555,33 @@ code {
|
|||
[data-theme='dark'] .folder-component-content table tr:nth-child(2n) {
|
||||
background-color: rgba(110, 118, 129, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ProgressInstall shimmer background */
|
||||
.progress-install-shimmer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
transparent 0%,
|
||||
color-mix(in srgb, var(--colors-primary-4) 8%, transparent) 25%,
|
||||
color-mix(in srgb, var(--colors-primary-4) 20%, transparent) 50%,
|
||||
color-mix(in srgb, var(--colors-primary-4) 8%, transparent) 75%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
background-position: 200% 0;
|
||||
animation: progress-install-shimmer 3s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
border-radius: var(--borderRadius-lg);
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
@keyframes progress-install-shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
src/types/chatbox.d.ts
vendored
7
src/types/chatbox.d.ts
vendored
|
|
@ -12,7 +12,12 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import type { AgentStepType, AgentMessageStatusType, TaskStatusType, ChatTaskStatusType, AgentStatusType } from './constants';
|
||||
import type {
|
||||
AgentMessageStatusType,
|
||||
AgentStatusType,
|
||||
AgentStepType,
|
||||
TaskStatusType,
|
||||
} from './constants';
|
||||
|
||||
// Global type definitions for ChatBox component
|
||||
|
||||
|
|
|
|||
|
|
@ -16,32 +16,32 @@
|
|||
* SSE step values received from the backend in AgentMessage.step.
|
||||
*/
|
||||
export const AgentStep = {
|
||||
CONFIRMED: 'confirmed',
|
||||
NEW_TASK_STATE: 'new_task_state',
|
||||
END: 'end',
|
||||
WAIT_CONFIRM: 'wait_confirm',
|
||||
DECOMPOSE_TEXT: 'decompose_text',
|
||||
TO_SUB_TASKS: 'to_sub_tasks',
|
||||
CREATE_AGENT: 'create_agent',
|
||||
TASK_STATE: 'task_state',
|
||||
ACTIVATE_AGENT: 'activate_agent',
|
||||
DEACTIVATE_AGENT: 'deactivate_agent',
|
||||
ASSIGN_TASK: 'assign_task',
|
||||
ACTIVATE_TOOLKIT: 'activate_toolkit',
|
||||
DEACTIVATE_TOOLKIT: 'deactivate_toolkit',
|
||||
TERMINAL: 'terminal',
|
||||
WRITE_FILE: 'write_file',
|
||||
BUDGET_NOT_ENOUGH: 'budget_not_enough',
|
||||
CONTEXT_TOO_LONG: 'context_too_long',
|
||||
ERROR: 'error',
|
||||
ADD_TASK: 'add_task',
|
||||
REMOVE_TASK: 'remove_task',
|
||||
NOTICE: 'notice',
|
||||
ASK: 'ask',
|
||||
SYNC: 'sync',
|
||||
NOTICE_CARD: 'notice_card',
|
||||
FAILED: 'failed',
|
||||
AGENT_SUMMARY_END: 'agent_summary_end',
|
||||
CONFIRMED: 'confirmed',
|
||||
NEW_TASK_STATE: 'new_task_state',
|
||||
END: 'end',
|
||||
WAIT_CONFIRM: 'wait_confirm',
|
||||
DECOMPOSE_TEXT: 'decompose_text',
|
||||
TO_SUB_TASKS: 'to_sub_tasks',
|
||||
CREATE_AGENT: 'create_agent',
|
||||
TASK_STATE: 'task_state',
|
||||
ACTIVATE_AGENT: 'activate_agent',
|
||||
DEACTIVATE_AGENT: 'deactivate_agent',
|
||||
ASSIGN_TASK: 'assign_task',
|
||||
ACTIVATE_TOOLKIT: 'activate_toolkit',
|
||||
DEACTIVATE_TOOLKIT: 'deactivate_toolkit',
|
||||
TERMINAL: 'terminal',
|
||||
WRITE_FILE: 'write_file',
|
||||
BUDGET_NOT_ENOUGH: 'budget_not_enough',
|
||||
CONTEXT_TOO_LONG: 'context_too_long',
|
||||
ERROR: 'error',
|
||||
ADD_TASK: 'add_task',
|
||||
REMOVE_TASK: 'remove_task',
|
||||
NOTICE: 'notice',
|
||||
ASK: 'ask',
|
||||
SYNC: 'sync',
|
||||
NOTICE_CARD: 'notice_card',
|
||||
FAILED: 'failed',
|
||||
AGENT_SUMMARY_END: 'agent_summary_end',
|
||||
} as const;
|
||||
|
||||
export type AgentStepType = (typeof AgentStep)[keyof typeof AgentStep];
|
||||
|
|
@ -50,24 +50,25 @@ export type AgentStepType = (typeof AgentStep)[keyof typeof AgentStep];
|
|||
* Status values on AgentMessage.status (SSE message lifecycle).
|
||||
*/
|
||||
export const AgentMessageStatus = {
|
||||
RUNNING: 'running',
|
||||
FILLED: 'filled',
|
||||
COMPLETED: 'completed',
|
||||
RUNNING: 'running',
|
||||
FILLED: 'filled',
|
||||
COMPLETED: 'completed',
|
||||
} as const;
|
||||
|
||||
export type AgentMessageStatusType = (typeof AgentMessageStatus)[keyof typeof AgentMessageStatus];
|
||||
export type AgentMessageStatusType =
|
||||
(typeof AgentMessageStatus)[keyof typeof AgentMessageStatus];
|
||||
|
||||
/**
|
||||
* Status values for TaskInfo (individual sub-task progress).
|
||||
*/
|
||||
export const TaskStatus = {
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
SKIPPED: 'skipped',
|
||||
WAITING: 'waiting',
|
||||
RUNNING: 'running',
|
||||
BLOCKED: 'blocked',
|
||||
EMPTY: '',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
SKIPPED: 'skipped',
|
||||
WAITING: 'waiting',
|
||||
RUNNING: 'running',
|
||||
BLOCKED: 'blocked',
|
||||
EMPTY: '',
|
||||
} as const;
|
||||
|
||||
export type TaskStatusType = (typeof TaskStatus)[keyof typeof TaskStatus];
|
||||
|
|
@ -76,22 +77,24 @@ export type TaskStatusType = (typeof TaskStatus)[keyof typeof TaskStatus];
|
|||
* Top-level task status in the ChatStore Task interface.
|
||||
*/
|
||||
export const ChatTaskStatus = {
|
||||
RUNNING: 'running',
|
||||
FINISHED: 'finished',
|
||||
PENDING: 'pending',
|
||||
PAUSE: 'pause',
|
||||
RUNNING: 'running',
|
||||
FINISHED: 'finished',
|
||||
PENDING: 'pending',
|
||||
PAUSE: 'pause',
|
||||
} as const;
|
||||
|
||||
export type ChatTaskStatusType = (typeof ChatTaskStatus)[keyof typeof ChatTaskStatus];
|
||||
export type ChatTaskStatusType =
|
||||
(typeof ChatTaskStatus)[keyof typeof ChatTaskStatus];
|
||||
|
||||
/**
|
||||
* Status values for individual agent lifecycle (toolkit operations, agent progress).
|
||||
*/
|
||||
export const AgentStatusValue = {
|
||||
PENDING: 'pending',
|
||||
RUNNING: 'running',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
PENDING: 'pending',
|
||||
RUNNING: 'running',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
} as const;
|
||||
|
||||
export type AgentStatusType = (typeof AgentStatusValue)[keyof typeof AgentStatusValue];
|
||||
export type AgentStatusType =
|
||||
(typeof AgentStatusValue)[keyof typeof AgentStatusValue];
|
||||
|
|
|
|||
1458
tailwind.config.js
1458
tailwind.config.js
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue