This commit is contained in:
elpresidank 2026-05-12 08:06:58 -05:00
parent e8c7a4f6e0
commit ffd97375a8
160 changed files with 6704 additions and 1895 deletions

View file

@ -57,7 +57,7 @@ function AgentPhaseBlock({
}) {
const [manualToggle, setManualToggle] = useState<boolean | null>(null);
if (!content && !isActive) return null;
if (content.length === 0 && !isActive) return null;
// Auto-expand while actively streaming; user can override
const expanded = manualToggle ?? isActive;
@ -104,12 +104,12 @@ function AgentPhaseBlock({
<Loader2 className="ml-auto h-3 w-3 animate-spin text-fg-subtle" />
)}
</button>
{expanded && (content || isActive) && (
{expanded && (content.length > 0 || isActive) && (
<div className="border-t border-border/50 px-3 py-2 text-xs leading-relaxed text-fg-muted">
<p className="whitespace-pre-wrap">
{content || (isActive ? "..." : "")}
{content.length > 0 ? content : isActive ? "..." : ""}
</p>
{isActive && content && (
{isActive && content.length > 0 && (
<span className="mt-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
)}
</div>
@ -124,7 +124,7 @@ function AgentPhaseBlock({
function MessageBubble({ msg, collection }: { msg: ChatMessage; collection: string }) {
const isUser = msg.role === "user";
const hasAgentPhases = msg.agentPhases != null;
const agentPhases = msg.agentPhases;
const isError = !isUser && msg.content.startsWith("Error:");
return (
@ -139,23 +139,23 @@ function MessageBubble({ msg, collection }: { msg: ChatMessage; collection: stri
)}
>
{/* Agent phase blocks (only for agent messages) */}
{hasAgentPhases && msg.agentPhases && (
{agentPhases !== undefined && (
<div className="mb-2 space-y-1.5">
<AgentPhaseBlock
phase="think"
icon={<Brain className="h-3 w-3" />}
label="Thinking"
content={msg.agentPhases.think}
content={agentPhases.think}
isActive={msg.activePhase === "think"}
/>
<AgentPhaseBlock
phase="observe"
icon={<Eye className="h-3 w-3" />}
label="Observing"
content={msg.agentPhases.observe}
content={agentPhases.observe}
isActive={msg.activePhase === "observe"}
/>
{msg.agentPhases.answer && (
{agentPhases.answer.length > 0 && (
<div className="flex items-center gap-1.5 px-1 pt-1 text-xs text-emerald-400">
<CheckCircle className="h-3 w-3" />
<span className="font-medium">Answer</span>
@ -174,19 +174,19 @@ function MessageBubble({ msg, collection }: { msg: ChatMessage; collection: stri
</div>
) : (
<div className="prose prose-sm max-w-none text-fg prose-headings:text-fg prose-strong:text-fg prose-p:my-1 prose-a:text-brand-400 prose-pre:bg-surface-200 prose-pre:text-fg prose-code:text-brand-300">
<Markdown>{msg.content || (msg.isStreaming ? "" : "(empty)")}</Markdown>
<Markdown>{msg.content.length > 0 ? msg.content : msg.isStreaming === true ? "" : "(empty)"}</Markdown>
</div>
)}
{/* Streaming indicator */}
{msg.isStreaming && (
{msg.isStreaming === true && (
<span className="mt-1 inline-block h-2 w-2 animate-pulse rounded-full bg-brand-400" />
)}
{/* Token metadata */}
{msg.metadata && (
{msg.metadata !== undefined && (
<div className="mt-2 flex items-center gap-3 text-[10px] text-fg-subtle">
{msg.metadata.model && <span>{msg.metadata.model}</span>}
{msg.metadata.model !== undefined && msg.metadata.model.length > 0 && <span>{msg.metadata.model}</span>}
{msg.metadata.inTokens != null && (
<span>in: {msg.metadata.inTokens}</span>
)}
@ -197,7 +197,7 @@ function MessageBubble({ msg, collection }: { msg: ChatMessage; collection: stri
)}
{/* Explainability graph */}
{!isUser && !isError && !msg.isStreaming && msg.explainEvents && msg.explainEvents.length > 0 && (
{!isUser && !isError && msg.isStreaming !== true && msg.explainEvents !== undefined && msg.explainEvents.length > 0 && (
<ExplainGraph explainEvents={msg.explainEvents} collection={collection} />
)}
</div>
@ -239,7 +239,7 @@ export default function ChatPage() {
}, [messages]);
const handleSubmit = useCallback(() => {
if (input.trim()) {
if (input.trim().length > 0) {
submitMessage({ input });
}
}, [input, submitMessage]);
@ -317,12 +317,12 @@ export default function ChatPage() {
return (
<div key={msg.id} className="group relative">
{!msg.isStreaming && (
{msg.isStreaming !== true && (
<MessageActions
content={msg.content}
isLastAssistant={isLastAssistant}
onDelete={() => deleteMessage(msg.id)}
onRegenerate={isLastAssistant ? regenerateLastMessage : undefined}
{...(isLastAssistant ? { onRegenerate: regenerateLastMessage } : {})}
/>
)}
<MessageBubble msg={msg} collection={collection} />
@ -359,7 +359,7 @@ export default function ChatPage() {
/>
<button
onClick={handleSubmit}
disabled={!input.trim() || isLoading}
disabled={input.trim().length === 0 || isLoading}
aria-label="Send message"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-brand-600 text-white transition-colors hover:bg-brand-500 disabled:opacity-40"
>

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import {
Workflow,
Plus,
@ -59,7 +59,7 @@ function StartFlowDialog({
.then((names) => {
const list = names ?? [];
setBlueprints(list);
if (list.length > 0 && !blueprint) {
if (list.length > 0 && blueprint.length === 0) {
setBlueprint(list[0]!);
}
})
@ -70,7 +70,7 @@ function StartFlowDialog({
// Fetch blueprint definition when selection changes
useEffect(() => {
if (!blueprint) {
if (blueprint.length === 0) {
setBlueprintDef(null);
return;
}
@ -86,11 +86,11 @@ function StartFlowDialog({
// Pre-populate parameters with defaults from the definition
const paramsDef =
def?.parameters ?? def?.params ?? def?.["parameters"] ?? def?.["params"];
if (paramsDef && typeof paramsDef === "object") {
if (paramsDef !== undefined && paramsDef !== null && typeof paramsDef === "object") {
const defaults: Record<string, unknown> = {};
const params = paramsDef as Record<string, unknown>;
for (const [key, val] of Object.entries(params)) {
if (val && typeof val === "object" && "default" in (val as Record<string, unknown>)) {
if (val !== null && typeof val === "object" && "default" in (val as Record<string, unknown>)) {
defaults[key] = (val as Record<string, unknown>).default;
}
}
@ -100,10 +100,10 @@ function StartFlowDialog({
}
})
.catch(() => {
if (!cancelled) setBlueprintDef(null);
if (cancelled === false) setBlueprintDef(null);
})
.finally(() => {
if (!cancelled) setLoadingDef(false);
if (cancelled === false) setLoadingDef(false);
});
return () => {
cancelled = true;
@ -195,7 +195,7 @@ function StartFlowDialog({
placeholder="my-flow-id"
className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
{submitted && !id.trim() && (
{submitted && id.trim().length === 0 && (
<p className="mt-1 text-xs text-red-400">Flow ID is required</p>
)}
</div>
@ -226,7 +226,7 @@ function StartFlowDialog({
))}
</select>
)}
{submitted && !blueprint && (
{submitted && blueprint.length === 0 && (
<p className="mt-1 text-xs text-red-400">Blueprint is required</p>
)}
@ -237,7 +237,7 @@ function StartFlowDialog({
</div>
)}
{blueprintDef && !loadingDef && (
{blueprintDef !== null && !loadingDef && (
<div className="mt-2 rounded-lg border border-border bg-surface-50 p-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-fg-muted">
<Info className="h-3.5 w-3.5 text-brand-400" />
@ -245,7 +245,7 @@ function StartFlowDialog({
</div>
{/* Description from definition */}
{!!(blueprintDef.description || blueprintDef.desc) && (
{(blueprintDef.description !== undefined || blueprintDef.desc !== undefined) && (
<p className="mt-1.5 text-xs text-fg-muted">
{String(blueprintDef.description ?? blueprintDef.desc)}
</p>
@ -258,7 +258,9 @@ function StartFlowDialog({
blueprintDef.params ??
blueprintDef["parameters"] ??
blueprintDef["params"];
if (!paramsDef || typeof paramsDef !== "object") return null;
if (paramsDef === undefined || paramsDef === null || typeof paramsDef !== "object") {
return null;
}
const entries = Object.entries(paramsDef as Record<string, unknown>);
if (entries.length === 0) return null;
return (
@ -267,16 +269,16 @@ function StartFlowDialog({
<div className="mt-1 space-y-1">
{entries.map(([name, schema]) => {
const s = schema as Record<string, unknown> | null;
const type = s?.type ? String(s.type) : undefined;
const defaultVal = s && "default" in s ? s.default : undefined;
const desc = s?.description ? String(s.description) : undefined;
const type = s?.type !== undefined ? String(s.type) : undefined;
const defaultVal = s !== null && "default" in s ? s.default : undefined;
const desc = s?.description !== undefined ? String(s.description) : undefined;
return (
<div
key={name}
className="flex flex-wrap items-baseline gap-x-2 text-xs"
>
<span className="font-mono font-medium text-fg">{name}</span>
{type && (
{type !== undefined && (
<span className="rounded bg-surface-200 px-1 py-0.5 text-[10px] text-fg-subtle">
{type}
</span>
@ -286,7 +288,7 @@ function StartFlowDialog({
default: <span className="font-mono">{JSON.stringify(defaultVal)}</span>
</span>
)}
{desc && <span className="text-fg-subtle">- {desc}</span>}
{desc !== undefined && <span className="text-fg-subtle">- {desc}</span>}
</div>
);
})}
@ -330,7 +332,7 @@ function StartFlowDialog({
placeholder="Human-readable description"
className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
{submitted && !description.trim() && (
{submitted && description.trim().length === 0 && (
<p className="mt-1 text-xs text-red-400">Description is required</p>
)}
</div>
@ -350,12 +352,12 @@ function StartFlowDialog({
rows={4}
className={cn(
"w-full resize-none rounded-lg border bg-surface-100 px-3 py-2 font-mono text-xs text-fg placeholder:text-fg-subtle focus:outline-none focus:ring-1",
paramsError
paramsError !== null
? "border-error focus:border-error focus:ring-error"
: "border-border focus:border-brand-500 focus:ring-brand-500",
)}
/>
{paramsError && (
{paramsError !== null && (
<p className="text-xs text-error">{paramsError}</p>
)}
</div>
@ -462,7 +464,7 @@ function FlowRow({
</div>
</td>
<td className="px-4 py-3 text-fg-muted">
{flow.description || "--"}
{(flow.description ?? "").length > 0 ? flow.description : "--"}
</td>
<td className="px-4 py-3">
<Badge variant="success">Running</Badge>
@ -550,7 +552,7 @@ export default function FlowsPage() {
};
const handleStop = async () => {
if (!stopTarget) return;
if (stopTarget === null || stopTarget.length === 0) return;
try {
await stopFlow(stopTarget);
notify.success("Flow stopped", `Flow "${stopTarget}" has been stopped.`);
@ -602,13 +604,13 @@ export default function FlowsPage() {
</div>
)}
{error && (
{error !== null && (
<p role="alert" className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
)}
{!loading && !error && flows.length === 0 && (
{!loading && error === null && flows.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center">
<Workflow className="mb-3 h-10 w-10 text-fg-subtle opacity-30" />
<p className="text-fg-subtle">No flows configured.</p>
@ -650,7 +652,7 @@ export default function FlowsPage() {
/>
<StopFlowDialog
open={stopTarget != null}
open={stopTarget !== null}
flowId={stopTarget ?? ""}
onClose={() => setStopTarget(null)}
onConfirm={handleStop}

View file

@ -18,8 +18,6 @@ import {
ArrowRight,
ArrowLeft,
Filter,
ChevronDown,
ChevronRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useSocket } from "@/providers/socket-provider";
@ -193,7 +191,10 @@ export default function GraphPage() {
const [objectFilter, setObjectFilter] = useState("");
const [tripleLimit, setTripleLimit] = useState(2000);
const [showLegend, setShowLegend] = useState(false);
const hasActiveFilters = subjectFilter || predicateFilter || objectFilter;
const hasActiveFilters =
subjectFilter.length > 0 ||
predicateFilter.length > 0 ||
objectFilter.length > 0;
const fgRef = useRef<ForceGraphMethods<GraphNode, GraphLink> | undefined>(
undefined,
@ -210,14 +211,14 @@ export default function GraphPage() {
// Ref callback — attaches ResizeObserver when the container mounts
const containerRef = useCallback((el: HTMLDivElement | null) => {
// Disconnect previous observer
if (roRef.current) {
if (roRef.current !== null) {
roRef.current.disconnect();
roRef.current = null;
}
if (!el) return;
if (el === null) return;
const ro = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
if (entry !== undefined) {
const { width, height } = entry.contentRect;
setContainerSize({ width: Math.floor(width), height: Math.floor(height) });
}
@ -236,9 +237,9 @@ export default function GraphPage() {
hasAutoFit.current = false;
const flow = socket.flow(flowId);
const s: Term | undefined = subjectFilter ? { t: "i", i: subjectFilter } : undefined;
const p: Term | undefined = predicateFilter ? { t: "i", i: predicateFilter } : undefined;
const o: Term | undefined = objectFilter ? { t: "i", i: objectFilter } : undefined;
const s: Term | undefined = subjectFilter.length > 0 ? { t: "i", i: subjectFilter } : undefined;
const p: Term | undefined = predicateFilter.length > 0 ? { t: "i", i: predicateFilter } : undefined;
const o: Term | undefined = objectFilter.length > 0 ? { t: "i", i: objectFilter } : undefined;
const result = await flow.triplesQuery(
s,
@ -281,7 +282,7 @@ export default function GraphPage() {
// Search filter -- highlight matching nodes
const searchLower = searchTerm.toLowerCase();
const matchingIds = useMemo(() => {
if (!searchLower) return new Set<string>();
if (searchLower.length === 0) return new Set<string>();
return new Set(
graphData.nodes
.filter(
@ -293,13 +294,17 @@ export default function GraphPage() {
);
}, [graphData.nodes, searchLower]);
const selectedLabel = selectedNode
const selectedLabel = selectedNode !== null
? labelMap.get(selectedNode) ?? localName(selectedNode)
: "";
// Auto-fit graph to view once data loads
useEffect(() => {
if (graphData.nodes.length > 0 && fgRef.current && !hasAutoFit.current) {
if (
graphData.nodes.length > 0 &&
fgRef.current !== undefined &&
hasAutoFit.current === false
) {
hasAutoFit.current = true;
// Wait for force simulation to settle briefly before fitting
const timer = setTimeout(() => fgRef.current?.zoomToFit(400, 40), 500);
@ -387,7 +392,14 @@ export default function GraphPage() {
const src = link.source as unknown as GraphNode;
const tgt = link.target as unknown as GraphNode;
if (!src.x || !tgt.x) return;
if (
src.x === undefined ||
src.y === undefined ||
tgt.x === undefined ||
tgt.y === undefined
) {
return;
}
const midX = ((src.x ?? 0) + (tgt.x ?? 0)) / 2;
const midY = ((src.y ?? 0) + (tgt.y ?? 0)) / 2;
@ -427,7 +439,7 @@ export default function GraphPage() {
aria-label="Search nodes"
className="w-48 rounded-lg border border-border bg-surface-100 py-1.5 pl-8 pr-3 text-xs text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
{searchTerm && (
{searchTerm.length > 0 && (
<button
onClick={() => setSearchTerm("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-fg-subtle hover:text-fg"
@ -610,7 +622,7 @@ export default function GraphPage() {
)}
{/* Content */}
{error && (
{error !== null && (
<p className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
@ -672,13 +684,14 @@ export default function GraphPage() {
backgroundColor="transparent"
cooldownTicks={100}
warmupTicks={30}
width={containerSize?.width}
height={containerSize?.height}
{...(containerSize !== null
? { width: containerSize.width, height: containerSize.height }
: {})}
/>
</Suspense>
{/* Search results badge overlay */}
{searchTerm && matchingIds.size > 0 && (
{searchTerm.length > 0 && matchingIds.size > 0 && (
<div className="absolute bottom-3 left-3">
<Badge variant="success">
{matchingIds.size} match{matchingIds.size > 1 ? "es" : ""}
@ -708,7 +721,7 @@ export default function GraphPage() {
)}
{/* Detail panel -- positioned absolutely so it overlays the graph */}
{selectedNode && (
{selectedNode !== null && (
<div className="absolute inset-y-0 right-0 z-10">
<NodeDetailPanel
nodeId={selectedNode}

View file

@ -130,7 +130,7 @@ export default function KnowledgeCoresPage() {
);
const handleDelete = useCallback(async () => {
if (!deleteTarget) return;
if (deleteTarget === null || deleteTarget.length === 0) return;
setActionInProgress(deleteTarget);
try {
await socket.knowledge().deleteKgCore(deleteTarget);
@ -179,13 +179,13 @@ export default function KnowledgeCoresPage() {
</div>
)}
{error && (
{error !== null && error.length > 0 && (
<p className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
)}
{!loading && !error && cores.length === 0 && (
{!loading && error === null && cores.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center">
<BrainCircuit className="mb-3 h-10 w-10 text-fg-subtle opacity-30" />
<p className="text-fg-subtle">No knowledge cores available.</p>

View file

@ -79,26 +79,27 @@ function UploadDialog({
const handleFile = useCallback((f: File) => {
setFile(f);
if (!titleRef.current) setTitle(f.name.replace(/\.[^/.]+$/, ""));
if (titleRef.current.length === 0) setTitle(f.name.replace(/\.[^/.]+$/, ""));
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const f = e.dataTransfer.files[0];
if (f) handleFile(f);
if (f !== undefined) handleFile(f);
}, [handleFile]);
const handleSubmit = async () => {
if (!file) return;
if (file === null) return;
setUploading(true);
try {
const base64 = await fileToBase64(file);
const tagList = tags
.split(",")
.map((t) => t.trim())
.filter(Boolean);
const mimeType = file.type || "application/octet-stream";
.filter((tag) => tag.length > 0);
const mimeType =
file.type.length > 0 ? file.type : "application/octet-stream";
if (base64.length > CHUNKED_UPLOAD_THRESHOLD) {
await onUploadChunked(base64, mimeType, title, comments, tagList, setProgress);
@ -115,6 +116,7 @@ function UploadDialog({
};
const progressPercent = progress
!== null
? Math.round((progress.chunksUploaded / Math.max(progress.chunksTotal, 1)) * 100)
: 0;
@ -142,7 +144,7 @@ function UploadDialog({
</button>
<button
onClick={handleSubmit}
disabled={!file || !title.trim() || uploading}
disabled={file === null || title.trim().length === 0 || uploading}
className="flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-500 disabled:opacity-40"
>
{uploading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
@ -177,7 +179,7 @@ function UploadDialog({
)}
>
<Upload className="mb-2 h-8 w-8 text-fg-subtle" />
{file ? (
{file !== null ? (
<div className="flex items-center gap-2 text-sm text-fg">
<FileText className="h-4 w-4" />
<span>{file.name}</span>
@ -207,15 +209,15 @@ function UploadDialog({
type="file"
className="hidden"
accept=".pdf,.txt,.md,.csv,.json,.xml,.html"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) handleFile(f);
}}
onChange={(e) => {
const f = e.target.files?.[0];
if (f !== undefined) handleFile(f);
}}
/>
</div>
{/* Upload progress bar */}
{uploading && progress && (
{uploading && progress !== null && (
<div className="mb-4 space-y-1.5">
<div className="flex items-center justify-between text-xs text-fg-muted">
<span>
@ -294,11 +296,11 @@ function DocumentDetailDialog({
loading?: boolean;
onClose: () => void;
}) {
if (!doc) return null;
if (doc === null) return null;
return (
<Dialog open={open} onClose={onClose} title="Document Details" className="max-w-xl">
{loadingMeta && (
{loadingMeta === true && (
<div className="mb-3 flex items-center gap-2 text-xs text-fg-subtle">
<Loader2 className="h-3 w-3 animate-spin" />
Loading full metadata...
@ -308,7 +310,9 @@ function DocumentDetailDialog({
{/* Title */}
<div>
<h3 className="mb-1 text-xs font-medium uppercase tracking-wider text-fg-subtle">Title</h3>
<p className="text-sm text-fg">{doc.title || "Untitled"}</p>
<p className="text-sm text-fg">
{(doc.title ?? "").length > 0 ? doc.title : "Untitled"}
</p>
</div>
{/* ID */}
@ -326,7 +330,7 @@ function DocumentDetailDialog({
</div>
{/* Comments */}
{doc.comments && (
{(doc.comments ?? "").length > 0 && (
<div>
<h3 className="mb-1 text-xs font-medium uppercase tracking-wider text-fg-subtle">Comments</h3>
<p className="text-sm text-fg-muted">{doc.comments}</p>
@ -334,13 +338,13 @@ function DocumentDetailDialog({
)}
{/* Tags */}
{doc.tags && doc.tags.length > 0 && (
{(doc.tags ?? []).length > 0 && (
<div>
<h3 className="mb-1 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wider text-fg-subtle">
<Tag className="h-3 w-3" /> Tags
</h3>
<div className="flex flex-wrap gap-1.5">
{doc.tags.map((tag) => (
{(doc.tags ?? []).map((tag) => (
<Badge key={tag} variant="info">{tag}</Badge>
))}
</div>
@ -360,7 +364,7 @@ function DocumentDetailDialog({
)}
{/* User */}
{doc.user && (
{(doc.user ?? "").length > 0 && (
<div>
<h3 className="mb-1 text-xs font-medium uppercase tracking-wider text-fg-subtle">Uploaded by</h3>
<p className="text-sm text-fg-muted">{doc.user}</p>
@ -368,7 +372,7 @@ function DocumentDetailDialog({
)}
{/* Raw metadata (if any RDF triples) */}
{doc.metadata && doc.metadata.length > 0 && (
{doc.metadata !== undefined && doc.metadata.length > 0 && (
<div>
<h3 className="mb-1 text-xs font-medium uppercase tracking-wider text-fg-subtle">
Metadata ({doc.metadata.length} triples)
@ -424,7 +428,9 @@ function ConfirmDeleteDialog({
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-error" />
<p className="text-sm text-fg-muted">
Are you sure you want to delete{" "}
<span className="font-medium text-fg">{docTitle || "this document"}</span>?
<span className="font-medium text-fg">
{docTitle.length > 0 ? docTitle : "this document"}
</span>?
This action cannot be undone.
</p>
</div>
@ -500,12 +506,14 @@ export default function LibraryPage() {
};
const handleDelete = async () => {
if (!deleteTarget?.id) {
const target = deleteTarget;
const targetId = target?.id ?? "";
if (target === null || targetId.length === 0) {
setDeleteTarget(null);
return;
}
try {
await removeDocument(deleteTarget.id, collection);
await removeDocument(targetId, collection);
notify.success("Document deleted");
} catch {
notify.error("Delete failed");
@ -517,10 +525,11 @@ export default function LibraryPage() {
async (doc: DocumentMetadata) => {
setDetailDoc(doc);
setDetailOpen(true);
if (doc.id) {
const id = doc.id ?? "";
if (id.length > 0) {
setLoadingDetail(true);
const fullMeta = await getDocumentMetadata(doc.id);
if (fullMeta) setDetailDoc(fullMeta);
const fullMeta = await getDocumentMetadata(id);
if (fullMeta !== null) setDetailDoc(fullMeta);
setLoadingDetail(false);
}
},
@ -538,13 +547,13 @@ export default function LibraryPage() {
if (kind.includes("text") || kind.includes("plain")) return "Text";
if (kind.includes("html")) return "HTML";
if (kind.includes("json")) return "JSON";
return kind || "--";
return kind.length > 0 ? kind : "--";
};
// Search/filter
const searchLower = searchTerm.toLowerCase();
const filteredDocuments = useMemo(() => {
if (!searchLower) return documents;
if (searchLower.length === 0) return documents;
return documents.filter((doc) => {
const title = (doc.title ?? "").toLowerCase();
const id = (doc.id ?? "").toLowerCase();
@ -603,7 +612,7 @@ export default function LibraryPage() {
aria-label="Search documents"
className="w-full rounded-lg border border-border bg-surface-100 py-2 pl-9 pr-9 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500"
/>
{searchTerm && (
{searchTerm.length > 0 && (
<button
onClick={() => setSearchTerm("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-subtle hover:text-fg"
@ -626,9 +635,11 @@ export default function LibraryPage() {
{processing.map((p) => (
<div key={p.id} className="flex items-center gap-2 text-xs text-fg-muted">
<FileType2 className="h-3 w-3" />
<span className="truncate">{p["document-id"] || p.id}</span>
<span className="truncate">
{(p["document-id"] ?? "").length > 0 ? p["document-id"] : p.id}
</span>
<Badge variant="info" className="ml-auto">
{p.flow || "processing"}
{p.flow.length > 0 ? p.flow : "processing"}
</Badge>
</div>
))}
@ -644,11 +655,11 @@ export default function LibraryPage() {
</div>
)}
{error && (
{error !== null && (
<p className="py-8 text-center text-error">Error: {error}</p>
)}
{!loading && !error && documents.length === 0 && (
{!loading && error === null && documents.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center">
<LibraryBig className="mb-3 h-10 w-10 text-fg-subtle opacity-30" />
<p className="text-fg-subtle">
@ -658,7 +669,7 @@ export default function LibraryPage() {
)}
{/* Search results info */}
{searchTerm && documents.length > 0 && (
{searchTerm.length > 0 && documents.length > 0 && (
<p className="mb-2 text-xs text-fg-subtle">
{filteredDocuments.length} of {documents.length} documents match
</p>
@ -677,12 +688,12 @@ export default function LibraryPage() {
</tr>
</thead>
<tbody className="divide-y divide-border">
{filteredDocuments.map((doc) => (
<tr key={doc.id} className="hover:bg-surface-100/50">
{filteredDocuments.map((doc, index) => (
<tr key={doc.id ?? `${doc.title ?? "document"}-${index}`} className="hover:bg-surface-100/50">
<td className="px-4 py-3 text-fg">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 shrink-0 text-fg-subtle" />
{doc.title || "Untitled"}
{(doc.title ?? "").length > 0 ? doc.title : "Untitled"}
</div>
</td>
<td className="px-4 py-3">
@ -693,7 +704,7 @@ export default function LibraryPage() {
{(doc.tags ?? []).map((tag) => (
<Badge key={tag} variant="info">{tag}</Badge>
))}
{(!doc.tags || doc.tags.length === 0) && (
{(doc.tags ?? []).length === 0 && (
<span className="text-fg-subtle">--</span>
)}
</div>
@ -729,7 +740,7 @@ export default function LibraryPage() {
)}
{/* Empty search results */}
{searchTerm && filteredDocuments.length === 0 && documents.length > 0 && (
{searchTerm.length > 0 && filteredDocuments.length === 0 && documents.length > 0 && (
<div className="flex flex-1 flex-col items-center justify-center py-12">
<Search className="mb-3 h-8 w-8 text-fg-subtle opacity-30" />
<p className="text-fg-subtle">No documents match "{searchTerm}"</p>
@ -746,7 +757,7 @@ export default function LibraryPage() {
/>
<ConfirmDeleteDialog
open={deleteTarget != null}
open={deleteTarget !== null}
docTitle={deleteTarget?.title ?? deleteTarget?.id ?? ""}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import {
Plug,
Server,
@ -55,7 +55,7 @@ function McpServerDialog({
initial?: McpServerEntry;
existingKeys: string[];
}) {
const isEditing = initial != null;
const isEditing = initial !== undefined;
const [key, setKey] = useState(initial?.key ?? "");
const [url, setUrl] = useState(initial?.config.url ?? "");
const [remoteName, setRemoteName] = useState(
@ -81,14 +81,14 @@ function McpServerDialog({
}, [open, initial]);
const handleSave = async () => {
if (!key.trim() || !url.trim()) return;
if (key.trim().length === 0 || url.trim().length === 0) return;
if (!isEditing && existingKeys.includes(key.trim())) {
setKeyError("A server with this key already exists");
return;
}
const config: McpServerConfig = { url: url.trim() };
if (remoteName.trim()) config["remote-name"] = remoteName.trim();
if (authToken.trim()) config["auth-token"] = authToken.trim();
if (remoteName.trim().length > 0) config["remote-name"] = remoteName.trim();
if (authToken.trim().length > 0) config["auth-token"] = authToken.trim();
setSaving(true);
try {
await onSave(key.trim(), config);
@ -113,7 +113,7 @@ function McpServerDialog({
</button>
<button
onClick={handleSave}
disabled={!key.trim() || !url.trim() || saving}
disabled={key.trim().length === 0 || url.trim().length === 0 || saving}
className="flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-500 disabled:opacity-40"
>
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
@ -139,7 +139,7 @@ function McpServerDialog({
placeholder="brave-search"
className={cn(INPUT_CLASS, isEditing && "opacity-60")}
/>
{keyError && (
{keyError.length > 0 && (
<p className="mt-1 text-xs text-error">{keyError}</p>
)}
</div>
@ -220,7 +220,7 @@ function McpToolDialog({
existingKeys: string[];
serverKeys: string[];
}) {
const isEditing = initial != null;
const isEditing = initial !== undefined;
const [key, setKey] = useState(initial?.key ?? "");
const [name, setName] = useState(initial?.config.name ?? "");
const [description, setDescription] = useState(
@ -259,7 +259,11 @@ function McpToolDialog({
setArgs((prev) => prev.filter((_, j) => j !== i));
const handleSave = async () => {
if (!key.trim() || !name.trim() || !mcpTool.trim()) return;
if (
key.trim().length === 0 ||
name.trim().length === 0 ||
mcpTool.trim().length === 0
) return;
if (!isEditing && existingKeys.includes(key.trim())) {
setKeyError("A tool with this key already exists");
return;
@ -272,8 +276,8 @@ function McpToolDialog({
group: group
.split(",")
.map((g) => g.trim())
.filter(Boolean),
arguments: args.filter((a) => a.name.trim()),
.filter((g) => g.length > 0),
arguments: args.filter((a) => a.name.trim().length > 0),
};
setSaving(true);
try {
@ -300,7 +304,12 @@ function McpToolDialog({
</button>
<button
onClick={handleSave}
disabled={!key.trim() || !name.trim() || !mcpTool.trim() || saving}
disabled={
key.trim().length === 0 ||
name.trim().length === 0 ||
mcpTool.trim().length === 0 ||
saving
}
className="flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-500 disabled:opacity-40"
>
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
@ -327,7 +336,7 @@ function McpToolDialog({
placeholder="brave-search"
className={cn(INPUT_CLASS, isEditing && "opacity-60")}
/>
{keyError && (
{keyError.length > 0 && (
<p className="mt-1 text-xs text-error">{keyError}</p>
)}
</div>
@ -740,7 +749,11 @@ export default function McpToolsPage() {
referencingTools.length > 0
? `The following tools reference this server and will stop working: ${referencingTools.join(", ")}`
: undefined;
setDeleteTarget({ type: "server", key, warning });
setDeleteTarget({
type: "server",
key,
...(warning !== undefined ? { warning } : {}),
});
};
const openAddTool = () => {
@ -774,7 +787,7 @@ export default function McpToolsPage() {
);
const handleDelete = useCallback(async () => {
if (!deleteTarget) return;
if (deleteTarget === null) return;
try {
if (deleteTarget.type === "server") {
await deleteServer(deleteTarget.key);
@ -901,7 +914,7 @@ export default function McpToolsPage() {
</div>
{/* Error */}
{error && (
{error !== null && error.length > 0 && (
<p role="alert" className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
@ -988,24 +1001,24 @@ export default function McpToolsPage() {
open={serverDialogOpen}
onClose={() => setServerDialogOpen(false)}
onSave={handleSaveServer}
initial={editingServer}
existingKeys={serverKeys}
{...(editingServer !== undefined ? { initial: editingServer } : {})}
/>
<McpToolDialog
open={toolDialogOpen}
onClose={() => setToolDialogOpen(false)}
onSave={handleSaveTool}
initial={editingTool}
existingKeys={toolKeys}
serverKeys={serverKeys}
{...(editingTool !== undefined ? { initial: editingTool } : {})}
/>
<DeleteConfirmDialog
open={deleteTarget != null}
open={deleteTarget !== null}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}
entityType={deleteTarget?.type === "server" ? "MCP Server" : "Tool"}
entityKey={deleteTarget?.key ?? ""}
warning={deleteTarget?.warning}
{...(deleteTarget?.warning !== undefined ? { warning: deleteTarget.warning } : {})}
/>
</div>
);

View file

@ -101,7 +101,7 @@ export default function PromptsPage() {
</div>
{/* Error display */}
{error && (
{error !== null && error.length > 0 && (
<p className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
@ -157,7 +157,7 @@ export default function PromptsPage() {
{/* Prompt detail */}
<div className="flex flex-1 flex-col overflow-hidden rounded-lg border border-border">
{selectedPromptId ? (
{selectedPromptId !== null && selectedPromptId.length > 0 ? (
<>
<div className="flex items-center justify-between border-b border-border bg-surface-100 px-4 py-3">
<h2 className="text-sm font-medium text-fg">
@ -179,9 +179,9 @@ export default function PromptsPage() {
<Loader2 className="h-4 w-4 animate-spin" />
Loading...
</div>
) : promptDetail && typeof promptDetail === "object" ? (
) : promptDetail !== null && typeof promptDetail === "object" ? (
<div className="space-y-4">
{promptDetail.system && (
{promptDetail.system !== undefined && promptDetail.system.length > 0 && (
<section>
<h3 className="mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-fg-subtle">
System
@ -191,7 +191,7 @@ export default function PromptsPage() {
</pre>
</section>
)}
{promptDetail.prompt && (
{promptDetail.prompt !== undefined && promptDetail.prompt.length > 0 && (
<section>
<h3 className="mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-fg-subtle">
Prompt
@ -234,7 +234,7 @@ export default function PromptsPage() {
<Loader2 className="h-4 w-4 animate-spin" />
Loading...
</div>
) : systemPrompt ? (
) : systemPrompt.length > 0 ? (
<pre className="whitespace-pre-wrap font-mono text-xs text-fg-muted">
{systemPrompt}
</pre>

View file

@ -101,7 +101,7 @@ export default function SettingsPage() {
const [isDark, setIsDark] = useState(() => {
if (typeof window === "undefined") return true;
const saved = localStorage.getItem("tg-theme");
if (saved) return saved === "dark";
if (saved !== null) return saved === "dark";
return !document.documentElement.classList.contains("light");
});
@ -164,21 +164,21 @@ export default function SettingsPage() {
// Create a new collection
const handleCreateCollection = useCallback(async () => {
const trimmedId = newId.trim();
if (!trimmedId) return;
if (trimmedId.length === 0) return;
setCreating(true);
try {
const tags = newTags
.split(",")
.map((t) => t.trim())
.filter(Boolean);
.filter((tag) => tag.length > 0);
await socket
.collectionManagement()
.updateCollection(
trimmedId,
newName.trim() || undefined,
newDescription.trim() || undefined,
newName.trim().length > 0 ? newName.trim() : undefined,
newDescription.trim().length > 0 ? newDescription.trim() : undefined,
tags.length > 0 ? tags : undefined,
);
@ -205,7 +205,7 @@ export default function SettingsPage() {
// Delete the current collection
const handleDeleteCollection = useCallback(async () => {
const currentId = settings.collection;
if (!currentId) return;
if (currentId.length === 0) return;
setDeleting(true);
try {
@ -432,7 +432,7 @@ export default function SettingsPage() {
</button>
<button
type="button"
disabled={!newId.trim() || creating}
disabled={newId.trim().length === 0 || creating}
onClick={handleCreateCollection}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
@ -561,7 +561,7 @@ export default function SettingsPage() {
{flows.map((f) => (
<option key={f.id} value={f.id}>
{f.id}
{f.description ? ` -- ${f.description}` : ""}
{f.description !== undefined && f.description.length > 0 ? ` -- ${f.description}` : ""}
</option>
))}
</select>
@ -621,28 +621,31 @@ export default function SettingsPage() {
title="Feature Switches"
icon={<SettingsIcon className="h-4 w-4 text-fg-subtle" />}
>
{Object.entries(settings.featureSwitches).map(([key, enabled]) => (
<div key={key} className="flex items-center justify-between">
<div>
<p className="text-sm text-fg">{featureLabel(key)}</p>
{Object.entries(settings.featureSwitches).map(([key, enabled]) => {
const isEnabled = enabled === true;
return (
<div key={key} className="flex items-center justify-between">
<div>
<p className="text-sm text-fg">{featureLabel(key)}</p>
</div>
<button
role="switch"
aria-checked={isEnabled}
aria-label={featureLabel(key)}
onClick={() => updateFeatureSwitches({ [key]: !isEnabled })}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
isEnabled ? "bg-brand-600" : "bg-fg-subtle",
)}
>
<span className={cn(
"inline-block h-4 w-4 rounded-full bg-white transition-transform",
isEnabled ? "translate-x-6" : "translate-x-1",
)} />
</button>
</div>
<button
role="switch"
aria-checked={enabled}
aria-label={featureLabel(key)}
onClick={() => updateFeatureSwitches({ [key]: !enabled })}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
enabled ? "bg-brand-600" : "bg-fg-subtle",
)}
>
<span className={cn(
"inline-block h-4 w-4 rounded-full bg-white transition-transform",
enabled ? "translate-x-6" : "translate-x-1",
)} />
</button>
</div>
))}
);
})}
</Section>
{/* About */}

View file

@ -60,7 +60,7 @@ export default function TokenCostPage() {
}, [connectionState.status, loadCosts]);
const formatPrice = (price: number) => {
if (price == null) return "--";
if (!Number.isFinite(price)) return "--";
return `$${price.toFixed(2)}`;
};
@ -96,13 +96,13 @@ export default function TokenCostPage() {
</div>
)}
{error && (
{error !== null && error.length > 0 && (
<p className="mb-4 rounded-lg bg-error/10 px-4 py-2 text-sm text-error">
{error}
</p>
)}
{!loading && !error && costs.length === 0 && (
{!loading && error === null && costs.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center">
<Coins className="mb-3 h-10 w-10 text-fg-subtle opacity-30" />
<p className="text-fg-subtle">No token cost data available.</p>