mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-17 01:58:47 +00:00
feat: add summary info
This commit is contained in:
@@ -469,13 +469,6 @@ async def skill_upload(
|
||||
return Result.failed(code="E5002", msg=f"Upload failed: {str(e)}")
|
||||
|
||||
|
||||
class _GitHubImportRequest(_BaseModel):
|
||||
"""Request body for importing a skill from a GitHub repository."""
|
||||
|
||||
github_url: str
|
||||
branch: str = "main"
|
||||
|
||||
|
||||
def _parse_github_url(
|
||||
github_url: str,
|
||||
) -> "tuple[str, str, str, Optional[str]]":
|
||||
@@ -658,201 +651,18 @@ def _extract_skill_from_zip(
|
||||
return skill_name
|
||||
|
||||
|
||||
@router.post("/v1/skills/import_from_github", response_model=Result)
|
||||
async def skill_import_from_github(
|
||||
body: _GitHubImportRequest,
|
||||
user_token: UserRequest = Depends(get_user_from_headers),
|
||||
):
|
||||
"""Import a skill directly from a public GitHub repository.
|
||||
|
||||
The endpoint downloads the repository as a ZIP archive using the GitHub
|
||||
archive API, extracts it, optionally navigates to a sub-directory,
|
||||
validates the presence of a ``SKILL.md`` file, and installs the skill
|
||||
into ``skills/user/<skill-name>/``.
|
||||
|
||||
Args:
|
||||
body (_GitHubImportRequest): ``github_url`` (required) and optional
|
||||
``branch`` (default ``"main"``).
|
||||
|
||||
Returns:
|
||||
Result: ``{"file_path": "<relative-path>", "message": "..."}`` on
|
||||
success, or a ``Result.failed`` with an appropriate error code.
|
||||
|
||||
Error codes:
|
||||
- ``E4003``: Malformed or non-GitHub URL.
|
||||
- ``E4004``: ``SKILL.md`` not found in the downloaded content.
|
||||
- ``E5003``: Network / download failure.
|
||||
- ``E5002``: Unexpected server-side error.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
# --- parse URL ----------------------------------------------------------
|
||||
try:
|
||||
owner, repo, branch_override, subdir = _parse_github_url(body.github_url)
|
||||
except ValueError as exc:
|
||||
return Result.failed(code="E4003", msg=str(exc))
|
||||
|
||||
branch = branch_override or body.branch
|
||||
|
||||
# --- build download URL -------------------------------------------------
|
||||
zip_url = f"https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip"
|
||||
|
||||
# --- download -----------------------------------------------------------
|
||||
skills_dir = Path(DEFAULT_SKILLS_DIR).expanduser().resolve()
|
||||
user_dir = skills_dir / "user"
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
upload_dir = Path(resolve_root_path("pilot/tmp") or "pilot/tmp").resolve()
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
follow_redirects=True,
|
||||
timeout=httpx.Timeout(120.0),
|
||||
) as client:
|
||||
response = await client.get(zip_url)
|
||||
if response.status_code != 200:
|
||||
return Result.failed(
|
||||
code="E5003",
|
||||
msg=(
|
||||
f"Failed to download {zip_url!r}: HTTP {response.status_code}"
|
||||
),
|
||||
)
|
||||
content_bytes = response.content
|
||||
except httpx.RequestError as exc:
|
||||
logger.exception("Network error while downloading skill from GitHub")
|
||||
return Result.failed(
|
||||
code="E5003", msg=f"Network error downloading skill: {str(exc)}"
|
||||
)
|
||||
|
||||
# --- save raw zip to tmp ------------------------------------------------
|
||||
zip_filename = f"{repo}-{branch}.zip"
|
||||
tmp_file = upload_dir / zip_filename
|
||||
tmp_file.write_bytes(content_bytes)
|
||||
|
||||
# --- extract & validate -------------------------------------------------
|
||||
try:
|
||||
buf = io.BytesIO(content_bytes)
|
||||
with zipfile.ZipFile(buf, "r") as zf:
|
||||
# Security: reject path-traversal entries
|
||||
for name in zf.namelist():
|
||||
if ".." in name or name.startswith("/"):
|
||||
return Result.failed(
|
||||
code="E4002",
|
||||
msg=f"Unsafe path in archive: {name}",
|
||||
)
|
||||
|
||||
all_names = zf.namelist()
|
||||
|
||||
# GitHub archives always have a single top-level dir:
|
||||
# "<repo>-<branch>/"
|
||||
top_dirs = {n.split("/")[0] for n in all_names if "/" in n}
|
||||
archive_root = top_dirs.pop() if len(top_dirs) == 1 else None
|
||||
|
||||
# Determine the directory inside the archive that holds SKILL.md
|
||||
if subdir:
|
||||
# User pointed at a sub-directory; look for SKILL.md there.
|
||||
if archive_root:
|
||||
skill_prefix = f"{archive_root}/{subdir}/"
|
||||
else:
|
||||
skill_prefix = f"{subdir}/"
|
||||
else:
|
||||
skill_prefix = f"{archive_root}/" if archive_root else ""
|
||||
|
||||
# Find the SKILL.md entry
|
||||
skill_md_entry = next(
|
||||
(
|
||||
n
|
||||
for n in all_names
|
||||
if n == skill_prefix + "SKILL.md"
|
||||
or (not skill_prefix and n.endswith("/SKILL.md"))
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if skill_md_entry is None:
|
||||
return Result.failed(
|
||||
code="E4004",
|
||||
msg=(
|
||||
"No SKILL.md found in the repository"
|
||||
+ (f" under '{subdir}'" if subdir else "")
|
||||
+ ". Make sure the skill directory contains a SKILL.md file."
|
||||
),
|
||||
)
|
||||
|
||||
# Determine skill name from SKILL.md frontmatter (best-effort)
|
||||
try:
|
||||
skill_md_bytes = zf.read(skill_md_entry)
|
||||
skill_md_text = skill_md_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
from dbgpt.agent.claude_skill import FileBasedSkill
|
||||
|
||||
# Write to a tmp location so FileBasedSkill can parse it
|
||||
tmp_skill_dir = upload_dir / f"_gh_parse_{uuid.uuid4().hex}"
|
||||
tmp_skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
tmp_skill_md = tmp_skill_dir / "SKILL.md"
|
||||
tmp_skill_md.write_text(skill_md_text, encoding="utf-8")
|
||||
fskill = FileBasedSkill(str(tmp_skill_md))
|
||||
skill_name = fskill.metadata.name
|
||||
shutil.rmtree(tmp_skill_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
# Fall back to repo name or subdir name
|
||||
skill_name = subdir.split("/")[-1] if subdir else repo
|
||||
|
||||
# Sanitise skill name to a safe directory name
|
||||
safe_name = re.sub(r"[^A-Za-z0-9_\-]", "_", skill_name)
|
||||
|
||||
dest = user_dir / safe_name
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Extract only the files that live under skill_prefix
|
||||
for member in all_names:
|
||||
if not member.startswith(skill_prefix) or member == skill_prefix:
|
||||
continue
|
||||
# Relative path inside the skill directory
|
||||
rel = member[len(skill_prefix) :]
|
||||
if not rel:
|
||||
continue
|
||||
target = dest / rel
|
||||
if member.endswith("/"):
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(zf.read(member))
|
||||
|
||||
rel_path = str(dest.relative_to(skills_dir))
|
||||
return Result.succ(
|
||||
{
|
||||
"file_path": rel_path,
|
||||
"message": f"Skill imported successfully from GitHub: {rel_path}",
|
||||
}
|
||||
)
|
||||
|
||||
except zipfile.BadZipFile as exc:
|
||||
logger.exception("Bad ZIP received from GitHub")
|
||||
return Result.failed(
|
||||
code="E5003", msg=f"Downloaded archive is not a valid ZIP: {str(exc)}"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to import skill from GitHub")
|
||||
return Result.failed(code="E5002", msg=f"Import failed: {str(exc)}")
|
||||
|
||||
|
||||
@router.post("/v1/skills/import_github", response_model=Result)
|
||||
async def skill_import_from_github_v2(
|
||||
request: Request,
|
||||
user_token: UserRequest = Depends(get_user_from_headers),
|
||||
):
|
||||
"""Import a skill from a GitHub or skills.sh URL (v2 — raw JSON body).
|
||||
"""Import a skill from a GitHub or skills.sh URL.
|
||||
|
||||
Accepts ``{ "url": "..." }`` from the frontend, downloads the repository
|
||||
ZIP, extracts the skill, installs it to ``skills/user/<name>/``, and
|
||||
returns a success response.
|
||||
|
||||
Compared to the v1 endpoint (``/v1/skills/import_from_github``), this
|
||||
endpoint:
|
||||
This endpoint:
|
||||
|
||||
- Accepts a raw JSON body ``{ "url": "..." }`` (no Pydantic model).
|
||||
- Supports branch fallback: tries ``main`` first, then ``master`` if 404.
|
||||
|
||||
@@ -65,6 +65,7 @@ export const ChatEn = {
|
||||
export_pdf: 'Export PDF',
|
||||
task_files: 'Task Files',
|
||||
web_preview: 'Web Preview',
|
||||
content_summary: 'Summary',
|
||||
skill_label: 'Skill',
|
||||
database_label: 'Database',
|
||||
waiting_to_start: 'Waiting to start...',
|
||||
|
||||
@@ -73,6 +73,7 @@ export const ChatZh: Resources['translation'] = {
|
||||
export_pdf: '导出 PDF',
|
||||
task_files: '任务文件',
|
||||
web_preview: '网页预览',
|
||||
content_summary: '摘要',
|
||||
skill_label: '技能',
|
||||
database_label: '数据库',
|
||||
waiting_to_start: '等待开始...',
|
||||
@@ -124,18 +125,18 @@ export const ChatZh: Resources['translation'] = {
|
||||
artifact_type_generic: '产物',
|
||||
download: '下载',
|
||||
download_as_zip: '下载为 ZIP',
|
||||
skill_added_success: '技能 "{{skillName}}" 已添加',
|
||||
added: '已添加',
|
||||
add: '添加',
|
||||
add_context: '添加上下文(文件、数据库、知识库)',
|
||||
select_skill: '选择技能',
|
||||
skill_selected: '技能:{{name}}',
|
||||
select_database: '选择数据库',
|
||||
database_selected: '数据库:{{name}}',
|
||||
select_knowledge: '选择知识库',
|
||||
knowledge_selected: '知识库:{{name}}',
|
||||
voice_input: '语音输入',
|
||||
voice_input_coming_soon: '语音输入即将上线',
|
||||
expand_panel: '展开面板',
|
||||
search_skill: '搜索技能',
|
||||
skill_added_success: '技能 "{{skillName}}" 已添加',
|
||||
added: '已添加',
|
||||
add: '添加',
|
||||
add_context: '添加上下文(文件、数据库、知识库)',
|
||||
select_skill: '选择技能',
|
||||
skill_selected: '技能:{{name}}',
|
||||
select_database: '选择数据库',
|
||||
database_selected: '数据库:{{name}}',
|
||||
select_knowledge: '选择知识库',
|
||||
knowledge_selected: '知识库:{{name}}',
|
||||
voice_input: '语音输入',
|
||||
voice_input_coming_soon: '语音输入即将上线',
|
||||
expand_panel: '展开面板',
|
||||
search_skill: '搜索技能',
|
||||
} as const;
|
||||
|
||||
@@ -205,7 +205,8 @@ const getFileTypeLabel = (fileName: string, t: any, mimeType?: string): string =
|
||||
return t('file_type_spreadsheet');
|
||||
if (ext === 'csv' || mimeType?.includes('csv')) return t('file_type_spreadsheet');
|
||||
if (ext === 'pdf' || mimeType?.includes('pdf')) return t('file_type_pdf');
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext) || mimeType?.includes('image')) return t('file_type_image');
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext) || mimeType?.includes('image'))
|
||||
return t('file_type_image');
|
||||
if (['doc', 'docx'].includes(ext) || mimeType?.includes('word')) return t('file_type_word');
|
||||
if (['txt', 'md'].includes(ext) || mimeType?.includes('text')) return t('file_type_text');
|
||||
return t('file_type_generic');
|
||||
@@ -473,7 +474,10 @@ const StepCard: React.FC<{
|
||||
}, []);
|
||||
const isThinkingStep =
|
||||
step.status === 'running' &&
|
||||
(step.title === t('thinking') || step.title === '思考中' || step.title === '正在思考中' || step.title?.toLowerCase() === 'thinking');
|
||||
(step.title === t('thinking') ||
|
||||
step.title === '思考中' ||
|
||||
step.title === '正在思考中' ||
|
||||
step.title?.toLowerCase() === 'thinking');
|
||||
if (isThinkingStep) {
|
||||
return (
|
||||
<div
|
||||
@@ -898,7 +902,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200 truncate'>
|
||||
{attachedSkill.name}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>{t('skill_label')}</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>{t('skill_label')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -911,9 +915,9 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200 truncate'>
|
||||
{attachedDb.db_name}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
|
||||
{attachedDb.db_type || t('database_label')}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
|
||||
{attachedDb.db_type || t('database_label')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -945,7 +949,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
<span className='text-sm text-blue-600 dark:text-blue-400'>{t('db_gpt_thinking')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className='text-sm'>{t('waiting_to_start')}</span>
|
||||
<span className='text-sm'>{t('waiting_to_start')}</span>
|
||||
)}
|
||||
{isWorking && stepThoughts?.[activeStepId || 'initial'] && (
|
||||
<ThoughtBubble text={stepThoughts[activeStepId || 'initial']} />
|
||||
|
||||
@@ -101,9 +101,13 @@ export interface ManusRightPanelProps {
|
||||
databaseName?: string;
|
||||
/** Skill name for the skill-preview tab (set when a skill is created/packaged) */
|
||||
skillName?: string | null;
|
||||
/** Summary content to display in the summary tab */
|
||||
summaryContent?: string;
|
||||
/** Whether the summary is currently streaming */
|
||||
isSummaryStreaming?: boolean;
|
||||
}
|
||||
|
||||
export type PanelView = 'execution' | 'files' | 'html-preview' | 'skill-preview';
|
||||
export type PanelView = 'execution' | 'files' | 'html-preview' | 'skill-preview' | 'summary';
|
||||
|
||||
// Get icon for step type
|
||||
const getStepTypeIcon = (type: StepType) => {
|
||||
@@ -1456,6 +1460,8 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
|
||||
databaseType,
|
||||
databaseName,
|
||||
skillName,
|
||||
summaryContent,
|
||||
isSummaryStreaming,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [inputCollapsed, setInputCollapsed] = useState(false);
|
||||
@@ -1661,7 +1667,7 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* View Toggle Tabs */}
|
||||
{((artifacts && artifacts.length > 0) || previewArtifact || skillName) && (
|
||||
{((artifacts && artifacts.length > 0) || previewArtifact || skillName || !!summaryContent) && (
|
||||
<div className='flex items-center gap-0 px-5 bg-white dark:bg-[#111217] border-b border-gray-200 dark:border-gray-800'>
|
||||
<button
|
||||
onClick={() => setPanelView('execution')}
|
||||
@@ -1715,6 +1721,23 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!!summaryContent && (
|
||||
<button
|
||||
onClick={() => setPanelView('summary')}
|
||||
className={classNames(
|
||||
'px-4 py-2.5 text-xs font-medium transition-colors relative',
|
||||
panelView === 'summary'
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300',
|
||||
)}
|
||||
>
|
||||
<FileTextOutlined className='mr-1.5' />
|
||||
{t('content_summary')}
|
||||
{panelView === 'summary' && (
|
||||
<div className='absolute bottom-0 left-0 right-0 h-[2px] bg-gray-900 dark:bg-gray-100 rounded-full' />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{previewArtifact && (
|
||||
<button
|
||||
onClick={() => setPanelView('html-preview')}
|
||||
@@ -1776,6 +1799,13 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : panelView === 'summary' && summaryContent ? (
|
||||
<div className='prose prose-sm dark:prose-invert max-w-none text-gray-800 dark:text-gray-200 leading-relaxed'>
|
||||
<MarkDownContext>{summaryContent}</MarkDownContext>
|
||||
{isSummaryStreaming && (
|
||||
<span className='inline-block w-1.5 h-4 bg-blue-500 animate-pulse ml-0.5 align-text-bottom' />
|
||||
)}
|
||||
</div>
|
||||
) : panelView === 'files' ? (
|
||||
<div className='space-y-0'>
|
||||
<div className='flex items-center gap-1 mb-4 bg-gray-100/80 dark:bg-gray-800/60 rounded-lg p-1'>
|
||||
@@ -1932,7 +1962,9 @@ const ManusRightPanel: React.FC<ManusRightPanelProps> = ({
|
||||
<div className='text-sm font-semibold text-gray-800 dark:text-gray-200 truncate'>
|
||||
{skillDisplayName}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>{t('skill_label')}</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
|
||||
{t('skill_label')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{skillDescription && (
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@@ -30,7 +29,7 @@ import {
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SkillItem {
|
||||
@@ -105,45 +104,56 @@ function Skills() {
|
||||
const [importLoading, setImportLoading] = useState(false);
|
||||
const [importForm] = Form.useForm();
|
||||
|
||||
const {
|
||||
data: skillsList = [],
|
||||
loading: listLoading,
|
||||
refresh: refreshList,
|
||||
} = useRequest<SkillItem[], []>(async () => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.API_BASE_URL ?? ''}/api/v1/skills/list`);
|
||||
if (response?.success && Array.isArray(response.data)) {
|
||||
return response.data as SkillItem[];
|
||||
}
|
||||
return [];
|
||||
} catch (err) {
|
||||
console.error('[Skills] Failed to fetch list:', err);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [skillsList, setSkillsList] = useState<SkillItem[]>([]);
|
||||
const [listLoading, setListLoading] = useState(false);
|
||||
const [skillDetail, setSkillDetail] = useState<SkillDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const listFetchCountRef = useRef(0);
|
||||
|
||||
const {
|
||||
data: skillDetail,
|
||||
loading: detailLoading,
|
||||
run: fetchDetail,
|
||||
mutate: setSkillDetail,
|
||||
} = useRequest<SkillDetail | null, [string, string]>(
|
||||
async (skillName: string, filePath: string) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.API_BASE_URL ?? ''}/api/v1/skills/detail`, {
|
||||
params: { skill_name: skillName, file_path: filePath },
|
||||
});
|
||||
if (response?.success && response.data) {
|
||||
return response.data as SkillDetail;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[Skills] Failed to fetch detail:', err);
|
||||
return null;
|
||||
const fetchSkillsList = useCallback(async () => {
|
||||
setListLoading(true);
|
||||
const currentFetch = ++listFetchCountRef.current;
|
||||
try {
|
||||
const response = (await axios.get(`${process.env.API_BASE_URL ?? ''}/api/v1/skills/list`)) as any;
|
||||
if (currentFetch !== listFetchCountRef.current) return;
|
||||
if (response?.success && Array.isArray(response.data)) {
|
||||
setSkillsList(response.data as SkillItem[]);
|
||||
} else {
|
||||
setSkillsList([]);
|
||||
}
|
||||
},
|
||||
{ manual: true },
|
||||
);
|
||||
} catch (err) {
|
||||
if (currentFetch !== listFetchCountRef.current) return;
|
||||
console.error('[Skills] Failed to fetch list:', err);
|
||||
setSkillsList([]);
|
||||
} finally {
|
||||
if (currentFetch === listFetchCountRef.current) {
|
||||
setListLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkillsList();
|
||||
}, [fetchSkillsList]);
|
||||
|
||||
const fetchDetail = useCallback(async (skillName: string, filePath: string) => {
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const response = await (axios.get(`${process.env.API_BASE_URL ?? ''}/api/v1/skills/detail`, {
|
||||
params: { skill_name: skillName, file_path: filePath },
|
||||
}) as Promise<any>);
|
||||
if (response?.success && response.data) {
|
||||
setSkillDetail(response.data as SkillDetail);
|
||||
} else {
|
||||
setSkillDetail(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Skills] Failed to fetch detail:', err);
|
||||
setSkillDetail(null);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
let list = skillsList;
|
||||
@@ -170,7 +180,7 @@ function Skills() {
|
||||
setDetailOpen(false);
|
||||
setSelectedSkill(null);
|
||||
setSkillDetail(null);
|
||||
}, [setSkillDetail]);
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback((skillId: string, checked: boolean) => {
|
||||
setEnabledMap(prev => ({ ...prev, [skillId]: checked }));
|
||||
@@ -241,9 +251,9 @@ function Skills() {
|
||||
message.success(t('skills_upload_success', { count: successCount }));
|
||||
setUploadOpen(false);
|
||||
setUploadFileList([]);
|
||||
refreshList();
|
||||
fetchSkillsList();
|
||||
}
|
||||
}, [uploadFileList, refreshList, t]);
|
||||
}, [uploadFileList, fetchSkillsList, t]);
|
||||
|
||||
const uploadProps: UploadProps = {
|
||||
multiple: true,
|
||||
@@ -269,12 +279,16 @@ function Skills() {
|
||||
try {
|
||||
const values = await importForm.validateFields();
|
||||
setImportLoading(true);
|
||||
const res = await axios.post('/api/v1/skills/import_github', { url: values.github_url }, { timeout: 60000 }) as any;
|
||||
const res = (await axios.post(
|
||||
'/api/v1/skills/import_github',
|
||||
{ url: values.github_url },
|
||||
{ timeout: 60000 },
|
||||
)) as any;
|
||||
if (res?.success) {
|
||||
message.success(t('skills_github_import_success'));
|
||||
setImportModalVisible(false);
|
||||
importForm.resetFields();
|
||||
refreshList();
|
||||
fetchSkillsList();
|
||||
} else {
|
||||
message.error(res?.err_msg || t('skills_github_import_failed'));
|
||||
}
|
||||
|
||||
@@ -1792,10 +1792,11 @@ const Playground: NextPage = () => {
|
||||
);
|
||||
setActiveMessageId(responseId);
|
||||
|
||||
if (payload.content) {
|
||||
if (payload.content && payload.content.trim()) {
|
||||
setStreamingSummary('');
|
||||
setSummaryComplete(false);
|
||||
setRightPanelTab('summary');
|
||||
setRightPanelView('summary');
|
||||
|
||||
const summaryText = cleanFinalContent(payload.content);
|
||||
const streamInterval = setInterval(() => {
|
||||
@@ -2718,7 +2719,6 @@ const Playground: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* Panel toggle handle — placed between panels to avoid overflow clipping */}
|
||||
<div className='relative z-20 flex-shrink-0'>
|
||||
@@ -2727,7 +2727,11 @@ const Playground: NextPage = () => {
|
||||
onClick={() => setRightPanelCollapsed(prev => !prev)}
|
||||
className='absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-4 h-8 flex items-center justify-center bg-white dark:bg-[#1a1b1e] border border-gray-200 dark:border-gray-700 rounded-full shadow-sm hover:bg-gray-100 dark:hover:bg-gray-800 hover:w-5 hover:shadow-md transition-all duration-200 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
>
|
||||
{rightPanelCollapsed ? <LeftOutlined style={{ fontSize: 10 }} /> : <RightOutlined style={{ fontSize: 10 }} />}
|
||||
{rightPanelCollapsed ? (
|
||||
<LeftOutlined style={{ fontSize: 10 }} />
|
||||
) : (
|
||||
<RightOutlined style={{ fontSize: 10 }} />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -2781,6 +2785,8 @@ const Playground: NextPage = () => {
|
||||
onPanelViewChange={setRightPanelView}
|
||||
previewArtifact={previewArtifact}
|
||||
skillName={createdSkillNames[activeViewMsg?.id || ''] || null}
|
||||
summaryContent={streamingSummary || activeViewMsg?.context || ''}
|
||||
isSummaryStreaming={!_summaryComplete && !!streamingSummary}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user