refactor(frontend): unify list and create pages with compact topbar layout
Move resource list and guide page titles into the shared topbar, introduce ListPageLayout for tighter spacing, and align knowledge edit with the prompt editor chrome. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -241,6 +241,26 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Resource list pages use a tighter content band below the shared topbar. */
|
||||||
|
.app-content:has([data-app-content="list"]) {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.app-content:has([data-app-content="list"]) {
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.app-content:has([data-app-content="list"]) {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar-subtle {
|
.scrollbar-subtle {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: color-mix(
|
scrollbar-color: color-mix(
|
||||||
|
|||||||
@@ -50,14 +50,20 @@ import type {
|
|||||||
|
|
||||||
import type { RuntimeMode } from "./types";
|
import type { RuntimeMode } from "./types";
|
||||||
|
|
||||||
export function EditorBackButton({ onClick }: { onClick: () => void }) {
|
export function EditorBackButton({
|
||||||
|
onClick,
|
||||||
|
ariaLabel = "返回助手列表",
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
ariaLabel?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label="返回助手列表"
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={20} />
|
<ChevronLeft size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
80
frontend/src/components/layout/list-page-layout.tsx
Normal file
80
frontend/src/components/layout/list-page-layout.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { PageTopbar, TopbarPortal } from "./topbar-portal";
|
||||||
|
|
||||||
|
export function ListPageLayout({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description?: ReactNode;
|
||||||
|
action?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const hasHeader = Boolean(description || action);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopbarPortal>
|
||||||
|
<PageTopbar title={title} />
|
||||||
|
</TopbarPortal>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-app-content="list"
|
||||||
|
className={cn(
|
||||||
|
"mx-auto flex w-full max-w-[1440px] flex-col gap-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasHeader && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4",
|
||||||
|
description && action
|
||||||
|
? "sm:justify-between"
|
||||||
|
: action
|
||||||
|
? "sm:justify-end"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description && (
|
||||||
|
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListPageSection({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl border border-hairline bg-card p-5 shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,10 +33,9 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { DataList } from "@/components/ui/data-list";
|
import { DataList } from "@/components/ui/data-list";
|
||||||
import {
|
import {
|
||||||
PageTopbar,
|
ListPageLayout,
|
||||||
TopbarPortal,
|
ListPageSection,
|
||||||
} from "@/components/layout/topbar-portal";
|
} from "@/components/layout/list-page-layout";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
|
||||||
import { FilterPills } from "@/components/ui/filter-pills";
|
import { FilterPills } from "@/components/ui/filter-pills";
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||||
@@ -538,9 +537,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除助手
|
// 删除助手
|
||||||
async function handleDelete(id: string) {
|
async function handleDelete(assistant: AssistantListItem) {
|
||||||
|
if (!window.confirm(`确认删除助手“${assistant.name}”?`)) return;
|
||||||
try {
|
try {
|
||||||
await assistantsApi.remove(id);
|
await assistantsApi.remove(assistant.id);
|
||||||
await loadAssistants();
|
await loadAssistants();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setListError(error instanceof Error ? error.message : "删除失败");
|
setListError(error instanceof Error ? error.message : "删除失败");
|
||||||
@@ -997,23 +997,17 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
|
|
||||||
if (view === "list") {
|
if (view === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
<ListPageLayout
|
||||||
<PageHeader
|
title="助手列表"
|
||||||
title="助手列表"
|
description="管理已有的视频助手,支持提示词、工作流、Dify 和 FastGPT 类型。"
|
||||||
description="管理已有的视频助手,支持提示词、工作流、Dify 和 FastGPT 类型。"
|
action={
|
||||||
action={
|
<Button className="w-full shrink-0 gap-2 sm:w-auto" onClick={startCreate}>
|
||||||
<Button
|
<Plus size={16} />
|
||||||
size="lg"
|
创建助手
|
||||||
className="w-full gap-2 sm:w-auto"
|
</Button>
|
||||||
onClick={startCreate}
|
}
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<ListPageSection>
|
||||||
创建助手
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
filters={
|
filters={
|
||||||
<FilterPills
|
<FilterPills
|
||||||
@@ -1039,6 +1033,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
loadingText="正在加载助手列表…"
|
loadingText="正在加载助手列表…"
|
||||||
error={listError}
|
error={listError}
|
||||||
onRetry={() => void loadAssistants()}
|
onRetry={() => void loadAssistants()}
|
||||||
|
onRowClick={handleEdit}
|
||||||
empty={{
|
empty={{
|
||||||
title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手",
|
title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手",
|
||||||
description:
|
description:
|
||||||
@@ -1116,12 +1111,19 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
align: "right",
|
align: "right",
|
||||||
cellClassName: "flex justify-end gap-2",
|
cellClassName: "flex justify-end gap-2",
|
||||||
cell: (assistant) => (
|
cell: (assistant) => (
|
||||||
<>
|
<div
|
||||||
|
className="flex justify-end gap-2"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
|
className="gap-1.5 border-hairline-strong text-xs text-muted-foreground hover:text-foreground"
|
||||||
onClick={() => handleEdit(assistant)}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleEdit(assistant);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Pencil size={14} />
|
<Pencil size={14} />
|
||||||
编辑
|
编辑
|
||||||
@@ -1134,6 +1136,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||||
aria-label={`${assistant.name} 更多操作`}
|
aria-label={`${assistant.name} 更多操作`}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal size={15} />
|
<MoreHorizontal size={15} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1154,7 +1157,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
onSelect={(event) => {
|
onSelect={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void handleDelete(assistant.id);
|
window.setTimeout(
|
||||||
|
() => void handleDelete(assistant),
|
||||||
|
0,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
@@ -1162,59 +1168,52 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</ListPageSection>
|
||||||
</div>
|
</ListPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view === "choose") {
|
if (view === "choose") {
|
||||||
return (
|
return (
|
||||||
<>
|
<ListPageLayout
|
||||||
<TopbarPortal>
|
title="创建助手"
|
||||||
<PageTopbar
|
className="max-w-[1180px]"
|
||||||
title="创建助手"
|
description="先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。"
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
className="w-full shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground sm:w-auto"
|
||||||
onClick={() => router.push("/assistants")}
|
onClick={() => router.push("/assistants")}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={16} />
|
<ChevronLeft size={16} />
|
||||||
返回列表
|
返回列表
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
</TopbarPortal>
|
<ListPageSection>
|
||||||
|
<label className="block">
|
||||||
|
<div className="mb-2 text-sm font-medium text-foreground">
|
||||||
|
助手名称
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={draftName}
|
||||||
|
autoFocus
|
||||||
|
onChange={(event) => setDraftName(event.target.value)}
|
||||||
|
placeholder="请输入助手名称"
|
||||||
|
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</ListPageSection>
|
||||||
|
|
||||||
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
|
<section className="flex flex-col gap-3">
|
||||||
<p className="max-w-2xl text-[15px] leading-7 text-muted-foreground">
|
<div className="text-sm font-medium text-foreground">构建方式</div>
|
||||||
先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<label className="block">
|
|
||||||
<div className="mb-2 text-sm font-medium text-foreground">
|
|
||||||
助手名称
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={draftName}
|
|
||||||
autoFocus
|
|
||||||
onChange={(event) => setDraftName(event.target.value)}
|
|
||||||
placeholder="请输入助手名称"
|
|
||||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
|
||||||
<div className="text-sm font-medium text-foreground">构建方式</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{assistantTypeOptions.map((option) => {
|
{assistantTypeOptions.map((option) => {
|
||||||
const selected = draftType === option.type;
|
const selected = draftType === option.type;
|
||||||
|
|
||||||
@@ -1264,21 +1263,19 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
{createError && (
|
{createError && (
|
||||||
<span className="text-xs text-destructive">{createError}</span>
|
<span className="text-xs text-destructive">{createError}</span>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
disabled={creating}
|
||||||
disabled={creating}
|
onClick={() => router.push("/assistants")}
|
||||||
onClick={() => router.push("/assistants")}
|
>
|
||||||
>
|
取消
|
||||||
取消
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
disabled={!draftName.trim() || !draftType || creating}
|
disabled={!draftName.trim() || !draftType || creating}
|
||||||
onClick={() => void confirmType()}
|
onClick={() => void confirmType()}
|
||||||
@@ -1290,9 +1287,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
|||||||
)}
|
)}
|
||||||
创建助手
|
创建助手
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</ListPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { DataList } from "@/components/ui/data-list";
|
import { DataList } from "@/components/ui/data-list";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -40,7 +39,17 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import {
|
||||||
|
AssistantIdentity,
|
||||||
|
EditorBackButton,
|
||||||
|
EditableTitle,
|
||||||
|
} from "@/components/assistant-editor/editor-controls";
|
||||||
|
import { SectionCard } from "@/components/editor/section-card";
|
||||||
|
import {
|
||||||
|
ListPageLayout,
|
||||||
|
ListPageSection,
|
||||||
|
} from "@/components/layout/list-page-layout";
|
||||||
|
import { TopbarPortal } from "@/components/layout/topbar-portal";
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -196,23 +205,20 @@ function KnowledgeListView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
<ListPageLayout
|
||||||
<PageHeader
|
title="知识库"
|
||||||
title="知识库"
|
description="管理可检索的文件与文字资料。"
|
||||||
description="管理可检索的文件与文字资料。"
|
action={
|
||||||
action={
|
<Button
|
||||||
<Button
|
className="w-full shrink-0 gap-2 sm:w-auto"
|
||||||
size="lg"
|
onClick={() => router.push("/components/knowledge/new")}
|
||||||
className="w-full gap-2 sm:w-auto"
|
>
|
||||||
onClick={() => router.push("/components/knowledge/new")}
|
<Plus size={16} />
|
||||||
>
|
添加知识库
|
||||||
<Plus size={16} />
|
</Button>
|
||||||
添加知识库
|
}
|
||||||
</Button>
|
>
|
||||||
}
|
<ListPageSection>
|
||||||
/>
|
|
||||||
|
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
className="lg:justify-end"
|
className="lg:justify-end"
|
||||||
search={
|
search={
|
||||||
@@ -375,8 +381,8 @@ function KnowledgeListView() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</ListPageSection>
|
||||||
</div>
|
</ListPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,26 +431,21 @@ function KnowledgeCreateView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
|
<ListPageLayout
|
||||||
<div className="flex items-start justify-between gap-6">
|
title="添加知识库"
|
||||||
<div>
|
className="max-w-[1180px]"
|
||||||
<h1 className="font-display display-lg text-ink">添加知识库</h1>
|
description="填写名称、说明并选择 Embedding 模型。确认后将立即创建并进入编辑页。"
|
||||||
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
|
action={
|
||||||
填写名称、说明并选择 Embedding 模型。确认后将立即创建并进入编辑页。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
className="w-full shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground sm:w-auto"
|
||||||
className="shrink-0 gap-2 border-hairline-strong text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={() => router.push("/components/knowledge")}
|
onClick={() => router.push("/components/knowledge")}
|
||||||
>
|
>
|
||||||
<ChevronLeft size={16} />
|
<ChevronLeft size={16} />
|
||||||
返回列表
|
返回列表
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Loader2 size={16} className="animate-spin" />
|
<Loader2 size={16} className="animate-spin" />
|
||||||
@@ -452,57 +453,65 @@ function KnowledgeCreateView() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
<ListPageSection>
|
||||||
<div className="space-y-5">
|
<label className="block">
|
||||||
<label className="block">
|
<div className="mb-2 text-sm font-medium text-foreground">
|
||||||
<div className="mb-2 text-sm font-medium text-foreground">
|
知识库名称
|
||||||
知识库名称
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
autoFocus
|
|
||||||
onChange={(event) => setName(event.target.value)}
|
|
||||||
placeholder="请输入知识库名称"
|
|
||||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="block">
|
|
||||||
<div className="mb-2 text-sm font-medium text-foreground">
|
|
||||||
描述
|
|
||||||
</div>
|
|
||||||
<Textarea
|
|
||||||
value={description}
|
|
||||||
onChange={(event) => setDescription(event.target.value)}
|
|
||||||
placeholder="用途说明(可选)"
|
|
||||||
rows={4}
|
|
||||||
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 text-sm font-medium text-foreground">
|
|
||||||
Embedding 模型
|
|
||||||
</div>
|
|
||||||
<Select value={embeddingId} onValueChange={setEmbeddingId}>
|
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
|
||||||
<SelectValue placeholder="选择 Embedding 模型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{models.map((model) => (
|
|
||||||
<SelectItem key={model.id} value={model.id}>
|
|
||||||
{model.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{models.length === 0 && (
|
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
|
||||||
请先在「模型资源」中添加并启用 Embedding 模型。
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
autoFocus
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</ListPageSection>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-3">
|
||||||
|
<div className="text-sm font-medium text-foreground">
|
||||||
|
Embedding 配置
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ListPageSection>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<label className="block">
|
||||||
|
<div className="mb-2 text-sm font-medium text-foreground">
|
||||||
|
描述
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
placeholder="用途说明(可选)"
|
||||||
|
rows={4}
|
||||||
|
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-sm font-medium text-foreground">
|
||||||
|
Embedding 模型
|
||||||
|
</div>
|
||||||
|
<Select value={embeddingId} onValueChange={setEmbeddingId}>
|
||||||
|
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||||
|
<SelectValue placeholder="选择 Embedding 模型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{models.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{models.length === 0 && (
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
请先在「模型资源」中添加并启用 Embedding 模型。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListPageSection>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
@@ -511,7 +520,6 @@ function KnowledgeCreateView() {
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
|
||||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
onClick={() => router.push("/components/knowledge")}
|
onClick={() => router.push("/components/knowledge")}
|
||||||
@@ -519,7 +527,6 @@ function KnowledgeCreateView() {
|
|||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
disabled={!name.trim() || !embeddingId || creating}
|
disabled={!name.trim() || !embeddingId || creating}
|
||||||
onClick={() => void confirmCreate()}
|
onClick={() => void confirmCreate()}
|
||||||
@@ -534,7 +541,7 @@ function KnowledgeCreateView() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ListPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,206 +781,219 @@ function KnowledgeEditView({ knowledgeBaseId }: { knowledgeBaseId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1280px] flex-col gap-6">
|
<>
|
||||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3">
|
<TopbarPortal>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex h-full min-w-0 flex-1 items-center justify-between gap-4 sm:-ml-2 lg:-ml-4">
|
||||||
<Button
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
variant="ghost"
|
<EditorBackButton
|
||||||
size="icon"
|
ariaLabel="返回知识库列表"
|
||||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
onClick={() => router.push("/components/knowledge")}
|
||||||
onClick={() => router.push("/components/knowledge")}
|
/>
|
||||||
aria-label="返回知识库列表"
|
<EditableTitle
|
||||||
>
|
value={base.name}
|
||||||
<ChevronLeft size={20} />
|
onChange={(value) => void renameBase(value)}
|
||||||
</Button>
|
placeholder="未命名知识库"
|
||||||
<EditableTitle value={base.name} onChange={(value) => void renameBase(value)} />
|
editLabel="知识库名称"
|
||||||
</div>
|
/>
|
||||||
|
<AssistantIdentity assistantId={knowledgeBaseId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 flex-wrap justify-end gap-2">
|
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||||||
<Button
|
{error && (
|
||||||
variant="outline"
|
<span
|
||||||
size="sm"
|
role="alert"
|
||||||
className="gap-1.5 border-hairline-strong text-muted-foreground hover:text-foreground"
|
title={error}
|
||||||
onClick={() => setSearchOpen(true)}
|
className="line-clamp-1 max-w-[min(32vw,420px)] text-right text-xs text-destructive"
|
||||||
>
|
>
|
||||||
<Search size={14} />
|
{error}
|
||||||
检索测试
|
</span>
|
||||||
</Button>
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
|
||||||
className="gap-1.5 border-hairline-strong text-muted-foreground hover:text-foreground"
|
onClick={() => setSearchOpen(true)}
|
||||||
onClick={openSettings}
|
|
||||||
>
|
|
||||||
<Settings size={14} />
|
|
||||||
设置
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button size="sm" className="gap-1.5" disabled={busy}>
|
|
||||||
<Upload size={14} />
|
|
||||||
{busy ? "上传中" : "上传"}
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
className="w-36 rounded-xl border border-hairline bg-popover p-1"
|
|
||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<Search size={16} />
|
||||||
className="rounded-lg"
|
检索测试
|
||||||
onSelect={() => fileInputRef.current?.click()}
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
|
||||||
|
onClick={openSettings}
|
||||||
|
>
|
||||||
|
<Settings size={16} />
|
||||||
|
设置
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button className="gap-2" disabled={busy}>
|
||||||
|
<Upload size={16} />
|
||||||
|
{busy ? "上传中" : "上传"}
|
||||||
|
<ChevronDown size={14} className="opacity-70" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-36 rounded-xl border border-hairline bg-popover p-1"
|
||||||
>
|
>
|
||||||
<Upload size={14} />
|
<DropdownMenuItem
|
||||||
上传文件
|
className="rounded-lg"
|
||||||
</DropdownMenuItem>
|
onSelect={() => fileInputRef.current?.click()}
|
||||||
<DropdownMenuItem
|
>
|
||||||
className="rounded-lg"
|
<Upload size={14} />
|
||||||
onSelect={() => setTextOpen(true)}
|
上传文件
|
||||||
>
|
</DropdownMenuItem>
|
||||||
<FileText size={14} />
|
<DropdownMenuItem
|
||||||
上传文本
|
className="rounded-lg"
|
||||||
</DropdownMenuItem>
|
onSelect={() => setTextOpen(true)}
|
||||||
</DropdownMenuContent>
|
>
|
||||||
</DropdownMenu>
|
<FileText size={14} />
|
||||||
<input
|
上传文本
|
||||||
ref={fileInputRef}
|
</DropdownMenuItem>
|
||||||
hidden
|
</DropdownMenuContent>
|
||||||
type="file"
|
</DropdownMenu>
|
||||||
accept=".pdf,.docx,.txt,.md,.csv,.json,.html,.htm"
|
</div>
|
||||||
disabled={busy}
|
</div>
|
||||||
onChange={(event) => {
|
</TopbarPortal>
|
||||||
void addFile(event.target.files?.[0]);
|
|
||||||
event.target.value = "";
|
<div
|
||||||
}}
|
data-app-content="full-bleed"
|
||||||
/>
|
className="flex h-full min-h-0 overflow-hidden bg-background"
|
||||||
|
>
|
||||||
|
<div className="scrollbar-subtle min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-none px-4 pb-6 pt-3 sm:px-6 sm:pb-8 lg:px-8 lg:pb-10">
|
||||||
|
<div className="mx-auto max-w-7xl space-y-3">
|
||||||
|
<SectionCard
|
||||||
|
icon={<FileText size={15} />}
|
||||||
|
title="文档"
|
||||||
|
description="上传文件或文本,系统将自动切分并生成向量。"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Search
|
||||||
|
className="absolute left-3 top-2.5 text-muted-foreground"
|
||||||
|
size={15}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="border-hairline-strong bg-background pl-9"
|
||||||
|
placeholder="搜索文档名称"
|
||||||
|
value={documentQuery}
|
||||||
|
onChange={(event) => setDocumentQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredDocuments.length === 0 && (
|
||||||
|
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
暂无文档,点击右上角「上传」开始入库。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{filteredDocuments.map((document) => {
|
||||||
|
const canPreview = document.status === "ready";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={document.id}
|
||||||
|
role={canPreview ? "button" : undefined}
|
||||||
|
tabIndex={canPreview ? 0 : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (canPreview) void previewDocument(document);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (
|
||||||
|
canPreview &&
|
||||||
|
(event.key === "Enter" || event.key === " ")
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
void previewDocument(document);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`rounded-xl border border-hairline p-3 transition-colors ${
|
||||||
|
canPreview
|
||||||
|
? "cursor-pointer hover:bg-surface-strong/40"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="truncate text-sm font-medium">
|
||||||
|
{document.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-2 py-0.5 text-xs ${statusClass(document.status)}`}
|
||||||
|
>
|
||||||
|
{statusLabel(document.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{document.sourceType === "file" ? "文件" : "文字"} ·{" "}
|
||||||
|
{formatBytes(document.sizeBytes)} ·{" "}
|
||||||
|
{document.chunkCount} 个分块
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex shrink-0 gap-1"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onKeyDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
{document.status === "failed" && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="重新处理"
|
||||||
|
onClick={() => void retryDocument(document.id)}
|
||||||
|
>
|
||||||
|
<RefreshCw size={15} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canPreview && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="查看分块"
|
||||||
|
onClick={() => void previewDocument(document)}
|
||||||
|
>
|
||||||
|
<Eye size={15} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="删除"
|
||||||
|
onClick={() => void removeDocument(document)}
|
||||||
|
>
|
||||||
|
<Trash2 size={15} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{document.status === "processing" && (
|
||||||
|
<div className="mt-2 flex items-center gap-2 text-xs text-amber-600">
|
||||||
|
<Loader2 className="animate-spin" size={13} />
|
||||||
|
正在抽取、切分并生成向量…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{document.errorMessage && (
|
||||||
|
<p className="mt-2 rounded bg-destructive/5 p-2 text-xs text-destructive">
|
||||||
|
{document.errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
<input
|
||||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
ref={fileInputRef}
|
||||||
{error}
|
hidden
|
||||||
</div>
|
type="file"
|
||||||
)}
|
accept=".pdf,.docx,.txt,.md,.csv,.json,.html,.htm"
|
||||||
|
disabled={busy}
|
||||||
<Card className="rounded-2xl border-hairline bg-card shadow-sm">
|
onChange={(event) => {
|
||||||
<CardHeader>
|
void addFile(event.target.files?.[0]);
|
||||||
<CardTitle className="text-base">文档</CardTitle>
|
event.target.value = "";
|
||||||
</CardHeader>
|
}}
|
||||||
<CardContent>
|
/>
|
||||||
<div className="relative mb-4">
|
|
||||||
<Search
|
|
||||||
className="absolute left-3 top-2.5 text-muted-foreground"
|
|
||||||
size={15}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="border-hairline-strong bg-background pl-9"
|
|
||||||
placeholder="搜索文档名称"
|
|
||||||
value={documentQuery}
|
|
||||||
onChange={(event) => setDocumentQuery(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{filteredDocuments.length === 0 && (
|
|
||||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
||||||
暂无文档,点击右上角「上传」开始入库。
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{filteredDocuments.map((document) => {
|
|
||||||
const canPreview = document.status === "ready";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={document.id}
|
|
||||||
role={canPreview ? "button" : undefined}
|
|
||||||
tabIndex={canPreview ? 0 : undefined}
|
|
||||||
onClick={() => {
|
|
||||||
if (canPreview) void previewDocument(document);
|
|
||||||
}}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (
|
|
||||||
canPreview &&
|
|
||||||
(event.key === "Enter" || event.key === " ")
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
void previewDocument(document);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`rounded-xl border border-hairline p-3 transition-colors ${
|
|
||||||
canPreview
|
|
||||||
? "cursor-pointer hover:bg-surface-strong/40"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<span className="truncate text-sm font-medium">
|
|
||||||
{document.name}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`rounded-full px-2 py-0.5 text-xs ${statusClass(document.status)}`}
|
|
||||||
>
|
|
||||||
{statusLabel(document.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
|
||||||
{document.sourceType === "file" ? "文件" : "文字"} ·{" "}
|
|
||||||
{formatBytes(document.sizeBytes)} ·{" "}
|
|
||||||
{document.chunkCount} 个分块
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex shrink-0 gap-1"
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
onKeyDown={(event) => event.stopPropagation()}
|
|
||||||
>
|
|
||||||
{document.status === "failed" && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
title="重新处理"
|
|
||||||
onClick={() => void retryDocument(document.id)}
|
|
||||||
>
|
|
||||||
<RefreshCw size={15} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canPreview && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
title="查看分块"
|
|
||||||
onClick={() => void previewDocument(document)}
|
|
||||||
>
|
|
||||||
<Eye size={15} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
title="删除"
|
|
||||||
onClick={() => void removeDocument(document)}
|
|
||||||
>
|
|
||||||
<Trash2 size={15} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{document.status === "processing" && (
|
|
||||||
<div className="mt-2 flex items-center gap-2 text-xs text-amber-600">
|
|
||||||
<Loader2 className="animate-spin" size={13} />
|
|
||||||
正在抽取、切分并生成向量…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{document.errorMessage && (
|
|
||||||
<p className="mt-2 rounded bg-destructive/5 p-2 text-xs text-destructive">
|
|
||||||
{document.errorMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
<Dialog open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||||
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-xl">
|
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-xl">
|
||||||
@@ -1152,75 +1172,7 @@ function KnowledgeEditView({ knowledgeBaseId }: { knowledgeBaseId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableTitle({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) {
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
const [draft, setDraft] = useState(value);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editing) {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
inputRef.current?.select();
|
|
||||||
}
|
|
||||||
}, [editing]);
|
|
||||||
|
|
||||||
function startEdit() {
|
|
||||||
setDraft(value);
|
|
||||||
setEditing(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function commit() {
|
|
||||||
const next = draft.trim();
|
|
||||||
if (next) onChange(next);
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editing) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
value={draft}
|
|
||||||
onChange={(event) => setDraft(event.target.value)}
|
|
||||||
onBlur={commit}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
commit();
|
|
||||||
} else if (event.key === "Escape") {
|
|
||||||
event.preventDefault();
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="font-display display-sm w-[min(60vw,420px)] border-b border-primary bg-transparent text-ink outline-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={startEdit}
|
|
||||||
title="点击修改知识库名称"
|
|
||||||
className="group -mx-2 flex min-w-0 items-center gap-2 rounded-lg px-2 py-1 text-left transition-colors hover:bg-surface-strong"
|
|
||||||
>
|
|
||||||
<span className="font-display display-sm truncate text-ink">
|
|
||||||
{value || "未命名知识库"}
|
|
||||||
</span>
|
|
||||||
<Pencil
|
|
||||||
size={16}
|
|
||||||
className="shrink-0 text-muted-soft opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DataList } from "@/components/ui/data-list";
|
import { DataList } from "@/components/ui/data-list";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import {
|
||||||
|
ListPageLayout,
|
||||||
|
ListPageSection,
|
||||||
|
} from "@/components/layout/list-page-layout";
|
||||||
import { FilterPills } from "@/components/ui/filter-pills";
|
import { FilterPills } from "@/components/ui/filter-pills";
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||||
@@ -495,23 +498,18 @@ export function ComponentsModelsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
<>
|
||||||
<PageHeader
|
<ListPageLayout
|
||||||
title="模型资源"
|
title="模型资源"
|
||||||
description="接口定义决定能力、鉴权字段和调用参数,表单会随接口类型动态更新。"
|
description="接口定义决定能力、鉴权字段和调用参数,表单会随接口类型动态更新。"
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button className="w-full shrink-0 gap-2 sm:w-auto" onClick={openCreate}>
|
||||||
size="lg"
|
|
||||||
className="w-full gap-2 sm:w-auto"
|
|
||||||
onClick={openCreate}
|
|
||||||
>
|
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
添加模型
|
添加模型
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<ListPageSection>
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
filters={
|
filters={
|
||||||
<FilterPills
|
<FilterPills
|
||||||
@@ -702,7 +700,8 @@ export function ComponentsModelsPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</ListPageSection>
|
||||||
|
</ListPageLayout>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-6xl lg:max-w-[88rem] lg:overflow-hidden">
|
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-6xl lg:max-w-[88rem] lg:overflow-hidden">
|
||||||
@@ -895,7 +894,7 @@ export function ComponentsModelsPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { FilterPills } from "@/components/ui/filter-pills";
|
import { FilterPills } from "@/components/ui/filter-pills";
|
||||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import {
|
||||||
|
ListPageLayout,
|
||||||
|
ListPageSection,
|
||||||
|
} from "@/components/layout/list-page-layout";
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -689,14 +692,14 @@ export function ComponentsToolsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
<>
|
||||||
<PageHeader
|
<ListPageLayout
|
||||||
title="工具资源"
|
title="工具资源"
|
||||||
description="管理可复用的助手工具,并将启用的工具绑定到提示词助手。"
|
description="管理可复用的助手工具,并将启用的工具绑定到提示词助手。"
|
||||||
action={
|
action={
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size="lg" className="w-full gap-2 sm:w-auto">
|
<Button className="w-full shrink-0 gap-2 sm:w-auto">
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
添加工具
|
添加工具
|
||||||
<ChevronDown size={14} className="opacity-70" />
|
<ChevronDown size={14} className="opacity-70" />
|
||||||
@@ -710,16 +713,18 @@ export function ComponentsToolsPage() {
|
|||||||
<Plus size={15} />
|
<Plus size={15} />
|
||||||
添加普通工具
|
添加普通工具
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="rounded-lg" onSelect={openCreateMcpServer}>
|
<DropdownMenuItem
|
||||||
|
className="rounded-lg"
|
||||||
|
onSelect={openCreateMcpServer}
|
||||||
|
>
|
||||||
<ServerCog size={15} />
|
<ServerCog size={15} />
|
||||||
添加 MCP 服务
|
添加 MCP 服务
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<ListPageSection>
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
filters={
|
filters={
|
||||||
<FilterPills options={toolFilters} value={filter} onChange={changeFilter} />
|
<FilterPills options={toolFilters} value={filter} onChange={changeFilter} />
|
||||||
@@ -758,7 +763,8 @@ export function ComponentsToolsPage() {
|
|||||||
: `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredResources.length)} / 共 ${filteredResources.length} 个工具资源`,
|
: `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredResources.length)} / 共 ${filteredResources.length} 个工具资源`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</ListPageSection>
|
||||||
|
</ListPageLayout>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className={TOOL_DIALOG_CONTENT_CLASS}>
|
<DialogContent className={TOOL_DIALOG_CONTENT_CLASS}>
|
||||||
@@ -866,7 +872,7 @@ export function ComponentsToolsPage() {
|
|||||||
onOpenChange={setMcpDialogOpen}
|
onOpenChange={setMcpDialogOpen}
|
||||||
onChanged={loadResources}
|
onChanged={loadResources}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { FilterPills } from "@/components/ui/filter-pills";
|
import { FilterPills } from "@/components/ui/filter-pills";
|
||||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import {
|
||||||
|
ListPageLayout,
|
||||||
|
ListPageSection,
|
||||||
|
} from "@/components/layout/list-page-layout";
|
||||||
import { SearchInput } from "@/components/ui/search-input";
|
import { SearchInput } from "@/components/ui/search-input";
|
||||||
import {
|
import {
|
||||||
conversationsApi,
|
conversationsApi,
|
||||||
@@ -364,13 +367,12 @@ export function HistoryPage() {
|
|||||||
const pageEnd = pageStart + rows.length;
|
const pageEnd = pageStart + rows.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8">
|
<>
|
||||||
<PageHeader
|
<ListPageLayout
|
||||||
title="历史记录"
|
title="历史记录"
|
||||||
description="查看每次语音或文字会话中最终确认的用户转写和助手回复。"
|
description="查看每次语音或文字会话中最终确认的用户转写和助手回复。"
|
||||||
/>
|
>
|
||||||
|
<ListPageSection>
|
||||||
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
|
|
||||||
<ListToolbar
|
<ListToolbar
|
||||||
filters={
|
filters={
|
||||||
<FilterPills
|
<FilterPills
|
||||||
@@ -417,7 +419,8 @@ export function HistoryPage() {
|
|||||||
: `显示 ${pageStart + 1}-${Math.min(pageEnd, total)} / 共 ${total} 次会话`,
|
: `显示 ${pageStart + 1}-${Math.min(pageEnd, total)} / 共 ${total} 次会话`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</ListPageSection>
|
||||||
|
</ListPageLayout>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-h-[calc(100vh-3rem)] grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden sm:max-w-4xl">
|
<DialogContent className="max-h-[calc(100vh-3rem)] grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden sm:max-w-4xl">
|
||||||
@@ -522,7 +525,7 @@ export function HistoryPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function ListToolbar({ filters, search, className }: ListToolbarProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between",
|
"mb-4 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user