mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
saving
This commit is contained in:
parent
e8c7a4f6e0
commit
ffd97375a8
160 changed files with 6704 additions and 1895 deletions
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue