chore(web): remove knowledge base module and fix worktree port conflicts
Remove the frontend-only knowledge base page, sidebar nav entry, and unused BookOpen import. Fix setup-worktree to always generate unique ports via init-worktree-env.sh instead of skipping when .env.worktree already exists with default ports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
88de467094
commit
c1fc5c7bca
3 changed files with 2 additions and 366 deletions
6
Makefile
6
Makefile
|
|
@ -95,10 +95,8 @@ check-main:
|
|||
@ENV_FILE=$(MAIN_ENV_FILE) bash scripts/check.sh
|
||||
|
||||
setup-worktree:
|
||||
@if [ ! -f "$(WORKTREE_ENV_FILE)" ]; then \
|
||||
echo "==> No $(WORKTREE_ENV_FILE) found, generating..."; \
|
||||
bash scripts/init-worktree-env.sh $(WORKTREE_ENV_FILE); \
|
||||
fi
|
||||
@echo "==> Generating $(WORKTREE_ENV_FILE) with unique ports..."
|
||||
@FORCE=1 bash scripts/init-worktree-env.sh $(WORKTREE_ENV_FILE)
|
||||
@$(MAKE) setup ENV_FILE=$(WORKTREE_ENV_FILE)
|
||||
|
||||
start-worktree:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
ListTodo,
|
||||
Bot,
|
||||
Monitor,
|
||||
BookOpen,
|
||||
ChevronDown,
|
||||
Settings,
|
||||
LogOut,
|
||||
|
|
@ -53,7 +52,6 @@ const workspaceNav = [
|
|||
{ href: "/agents", label: "Agents", icon: Bot },
|
||||
{ href: "/runtimes", label: "Runtimes", icon: Monitor },
|
||||
{ href: "/skills", label: "Skills", icon: Sparkles },
|
||||
{ href: "/knowledge-base", label: "Knowledge Base", icon: BookOpen },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,360 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useDefaultLayout } from "react-resizable-panels";
|
||||
import {
|
||||
FileText,
|
||||
Plus,
|
||||
Search,
|
||||
Link as LinkIcon,
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
ResizableHandle,
|
||||
} from "@/components/ui/resizable";
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
interface KBDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
createdBy: string;
|
||||
updatedAt: string;
|
||||
referencedBy: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simple Markdown-ish renderer (handles headers, code blocks, tables, lists)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderMarkdown(text: string): React.ReactNode[] {
|
||||
const lines = text.split("\n");
|
||||
const elements: React.ReactNode[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i]!;
|
||||
|
||||
// Code block
|
||||
if (line.startsWith("```")) {
|
||||
const lang = line.slice(3).trim();
|
||||
const codeLines: string[] = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i]!.startsWith("```")) {
|
||||
codeLines.push(lines[i]!);
|
||||
i++;
|
||||
}
|
||||
i++; // skip closing ```
|
||||
elements.push(
|
||||
<pre
|
||||
key={`code-${i}`}
|
||||
className="my-3 overflow-x-auto rounded-md bg-muted px-4 py-3 text-sm leading-relaxed"
|
||||
>
|
||||
<code>{codeLines.join("\n")}</code>
|
||||
</pre>
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Table (simplified: detect | pipes)
|
||||
if (line.includes("|") && line.trim().startsWith("|")) {
|
||||
const tableRows: string[] = [];
|
||||
while (i < lines.length && lines[i]!.includes("|") && lines[i]!.trim().startsWith("|")) {
|
||||
tableRows.push(lines[i]!);
|
||||
i++;
|
||||
}
|
||||
// Filter out separator rows (|---|---|)
|
||||
const dataRows = tableRows.filter((r) => !r.match(/^\|[\s-|]+\|$/));
|
||||
if (dataRows.length > 0) {
|
||||
const parseRow = (row: string) =>
|
||||
row.split("|").filter((c) => c.trim() !== "").map((c) => c.trim());
|
||||
const header = parseRow(dataRows[0]!);
|
||||
const body = dataRows.slice(1).map(parseRow);
|
||||
elements.push(
|
||||
<div key={`table-${i}`} className="my-3 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
{header.map((h, hi) => (
|
||||
<th key={hi} className="py-1.5 pr-4 text-left font-medium">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{body.map((row, ri) => (
|
||||
<tr key={ri} className="border-b last:border-0">
|
||||
{row.map((cell, ci) => (
|
||||
<td key={ci} className="py-1.5 pr-4 text-foreground/80">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Heading
|
||||
if (line.startsWith("## ")) {
|
||||
elements.push(
|
||||
<h2 key={`h2-${i}`} className="mt-6 mb-2 text-base font-semibold">
|
||||
{line.slice(3)}
|
||||
</h2>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("### ")) {
|
||||
elements.push(
|
||||
<h3 key={`h3-${i}`} className="mt-4 mb-1.5 text-sm font-medium">
|
||||
{line.slice(4)}
|
||||
</h3>,
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// List item
|
||||
if (/^- \[[ x]\] /.test(line)) {
|
||||
const checked = line.includes("[x]");
|
||||
const text = line.replace(/^- \[[ x]\] /, "");
|
||||
elements.push(
|
||||
<div key={`check-${i}`} className="flex items-center gap-2 py-0.5 text-sm text-foreground/80">
|
||||
<input type="checkbox" checked={checked} readOnly className="h-3.5 w-3.5 rounded" />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("- ")) {
|
||||
elements.push(
|
||||
<div key={`li-${i}`} className="flex gap-2 py-0.5 text-sm text-foreground/80">
|
||||
<span className="mt-2 h-1 w-1 shrink-0 rounded-full bg-foreground/40" />
|
||||
<span>{renderInline(line.slice(2))}</span>
|
||||
</div>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// Numbered list
|
||||
if (/^\d+\. /.test(line)) {
|
||||
const num = line.match(/^(\d+)\. /)![1]!;
|
||||
const text = line.replace(/^\d+\. /, "");
|
||||
elements.push(
|
||||
<div key={`ol-${i}`} className="flex gap-2 py-0.5 text-sm text-foreground/80">
|
||||
<span className="w-4 shrink-0 text-right text-muted-foreground">{num}.</span>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty line — guard is redundant since line is already asserted, but keeps TS happy
|
||||
if (line.trim() === "") {
|
||||
elements.push(<div key={`br-${i}`} className="h-2" />);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Paragraph
|
||||
elements.push(
|
||||
<p key={`p-${i}`} className="text-sm leading-relaxed text-foreground/85">
|
||||
{renderInline(line)}
|
||||
</p>
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
function renderInline(text: string): React.ReactNode {
|
||||
// Handle inline code `...`
|
||||
const parts = text.split(/(`[^`]+`)/);
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith("`") && part.endsWith("`")) {
|
||||
return (
|
||||
<code key={i} className="rounded bg-muted px-1 py-0.5 text-xs">
|
||||
{part.slice(1, -1)}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function DocListItem({
|
||||
doc,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {
|
||||
doc: KBDocument;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex w-full items-start gap-2.5 px-4 py-2.5 text-left transition-colors ${
|
||||
isSelected ? "bg-accent" : "hover:bg-accent/50"
|
||||
}`}
|
||||
>
|
||||
<FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">{doc.title}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{doc.createdBy}</span>
|
||||
<span>·</span>
|
||||
<span>{timeAgo(doc.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DocDetail({ doc }: { doc: KBDocument }) {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-3xl px-8 py-8">
|
||||
{/* Title */}
|
||||
<h1 className="text-xl font-semibold tracking-tight">{doc.title}</h1>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>By {doc.createdBy}</span>
|
||||
<span>·</span>
|
||||
<span>Updated {timeAgo(doc.updatedAt)}</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mt-6">{renderMarkdown(doc.content)}</div>
|
||||
|
||||
{/* Referenced by */}
|
||||
{doc.referencedBy.length > 0 && (
|
||||
<div className="mt-10 border-t pt-4">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<LinkIcon className="h-3 w-3" />
|
||||
<span>Referenced by</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{doc.referencedBy.map((ref) => (
|
||||
<span
|
||||
key={ref}
|
||||
className="rounded bg-muted px-2 py-0.5 text-xs font-mono"
|
||||
>
|
||||
{ref}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const [documents] = useState<KBDocument[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string>("");
|
||||
const [search, setSearch] = useState("");
|
||||
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
||||
id: "multica_kb_layout",
|
||||
});
|
||||
|
||||
const filtered = search
|
||||
? documents.filter((d) =>
|
||||
d.title.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: documents;
|
||||
|
||||
const selected = documents.find((d) => d.id === selectedId) ?? null;
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0" defaultLayout={defaultLayout} onLayoutChanged={onLayoutChanged}>
|
||||
<ResizablePanel id="list" defaultSize={280} minSize={240} maxSize={400} groupResizeBehavior="preserve-pixel-size">
|
||||
{/* Left: Document list */}
|
||||
<div className="overflow-y-auto h-full border-r">
|
||||
<div className="flex h-12 items-center justify-between border-b px-4">
|
||||
<h1 className="text-sm font-semibold">Knowledge Base</h1>
|
||||
<Button variant="ghost" size="icon-xs">
|
||||
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="border-b px-3 py-2">
|
||||
<div className="flex items-center gap-2 rounded-md border bg-background px-2.5 py-1.5">
|
||||
<Search className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search docs..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="border-0 bg-transparent shadow-none focus-visible:ring-0 flex-1 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document list */}
|
||||
<div className="divide-y">
|
||||
{filtered.map((doc) => (
|
||||
<DocListItem
|
||||
key={doc.id}
|
||||
doc={doc}
|
||||
isSelected={doc.id === selectedId}
|
||||
onClick={() => setSelectedId(doc.id)}
|
||||
/>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No documents found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle />
|
||||
|
||||
<ResizablePanel id="detail" minSize="50%">
|
||||
{/* Right: Document content */}
|
||||
{selected ? (
|
||||
<DocDetail doc={selected} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Select a document
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue