Skip to content

Commit 28c6427

Browse files
committed
refactor(kb): simplify wiring and reduce redundant fetches
- Add safePrompt/safeConfirm helpers to packages/kb-management/src/utils and use them in KB sidebar, resource inspector and settings view (removes 5 inline SSR guards). - Drive the linked-courses and linked-chatbots panels in the KB settings view from a single LINKED_CONSUMER_PANELS config entry (eliminates the near-duplicate Panel block). - Replace getOwnedKBOrThrow precheck with the lightweight assertOwnedKB in getKBResources, getKBIngestionRuns, deleteKB, updateKBRefreshPolicy, linkKBCourse, unlinkKBCourse, linkKBChatbot and unlinkKBChatbot (drops the wasted KB_INCLUDE round-trip when only ownership matters). - Run kbListQuery and selectedKbQuery refetches in parallel inside apps/frontend-kb runMutation. - Tighten the linkKBChatbot guard comment to a single sentence.
1 parent ecf4197 commit 28c6427

6 files changed

Lines changed: 85 additions & 84 deletions

File tree

apps/frontend-kb/src/pages/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ function KbHomePage() {
9595
}, [kbListQuery.data?.getKBs, selectedKnowledgeBaseId])
9696

9797
const refetchKBs = async () => {
98-
await kbListQuery.refetch()
99-
if (selectedKnowledgeBaseId) {
100-
await selectedKbQuery.refetch({ id: selectedKnowledgeBaseId })
101-
}
98+
await Promise.all([
99+
kbListQuery.refetch(),
100+
selectedKnowledgeBaseId
101+
? selectedKbQuery.refetch({ id: selectedKnowledgeBaseId })
102+
: Promise.resolve(),
103+
])
102104
}
103105

104106
const selectedKnowledgeBaseSummary =

packages/graphql/src/services/knowledge.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ export async function getKBResources(
526526
{ kbId, filter }: { kbId: string; filter?: KBResourceFilterInput | null },
527527
ctx: ContextWithUser
528528
) {
529-
await getOwnedKBOrThrow(kbId, ctx)
529+
await assertOwnedKB(kbId, ctx)
530530

531531
return await ctx.prisma.kBResource.findMany({
532532
where: {
@@ -556,7 +556,7 @@ export async function getKBIngestionRuns(
556556
}: { kbId: string; resourceId?: string | null; limit?: number | null },
557557
ctx: ContextWithUser
558558
) {
559-
await getOwnedKBOrThrow(kbId, ctx)
559+
await assertOwnedKB(kbId, ctx)
560560

561561
return await ctx.prisma.kBIngestionRun.findMany({
562562
where: { kbId, resourceId: resourceId ?? undefined },
@@ -670,7 +670,7 @@ export async function updateKB(
670670
}
671671

672672
export async function deleteKB({ id }: { id: string }, ctx: ContextWithUser) {
673-
await getOwnedKBOrThrow(id, ctx)
673+
await assertOwnedKB(id, ctx)
674674
const activeResources = await ctx.prisma.kBResource.findMany({
675675
where: { kbId: id, deletedAt: null },
676676
include: RESOURCE_INCLUDE,
@@ -848,7 +848,7 @@ export async function updateKBRefreshPolicy(
848848
{ kbId, input }: { kbId: string; input: UpdateKBRefreshPolicyInput },
849849
ctx: ContextWithUser
850850
) {
851-
await getOwnedKBOrThrow(kbId, ctx)
851+
await assertOwnedKB(kbId, ctx)
852852
const policy = validateKBRefreshPolicy(input)
853853
return await ctx.prisma.kB.update({
854854
where: { id: kbId },
@@ -877,7 +877,7 @@ export async function linkKBCourse(
877877
{ kbId, courseId }: { kbId: string; courseId: string },
878878
ctx: ContextWithUser
879879
) {
880-
await getOwnedKBOrThrow(kbId, ctx)
880+
await assertOwnedKB(kbId, ctx)
881881
const course = await ctx.prisma.course.findFirst({
882882
where: { id: courseId, ownerId: ctx.user.sub },
883883
select: { id: true },
@@ -897,7 +897,7 @@ export async function unlinkKBCourse(
897897
{ kbId, courseId }: { kbId: string; courseId: string },
898898
ctx: ContextWithUser
899899
) {
900-
await getOwnedKBOrThrow(kbId, ctx)
900+
await assertOwnedKB(kbId, ctx)
901901
await ctx.prisma.kBCourse.deleteMany({ where: { kbId, courseId } })
902902
return await getOwnedKBOrThrow(kbId, ctx)
903903
}
@@ -916,17 +916,15 @@ export async function linkKBChatbot(
916916
},
917917
ctx: ContextWithUser
918918
) {
919-
await getOwnedKBOrThrow(kbId, ctx)
919+
await assertOwnedKB(kbId, ctx)
920920
const chatbot = await ctx.prisma.chatbot.findFirst({
921921
where: { id: chatbotId, ownerId: ctx.user.sub },
922922
select: { id: true },
923923
})
924924
if (!chatbot) throw new Error('Chatbot not found')
925925

926-
// Enforce KB_PLAN.md decision #7: max one enabled KB per chatbot. When
927-
// enabling this link (default on create, explicit `true` on update), demote
928-
// every other enabled link for the same chatbot in the same transaction so
929-
// there is never a window with two active KBs.
926+
// KB_PLAN.md decision #7: at most one enabled KB per chatbot. No DB-level
927+
// partial unique index, so demote any other enabled rows in the same txn.
930928
const willEnable = isEnabled !== false
931929
await ctx.prisma.$transaction([
932930
...(willEnable
@@ -963,7 +961,7 @@ export async function unlinkKBChatbot(
963961
{ kbId, chatbotId }: { kbId: string; chatbotId: string },
964962
ctx: ContextWithUser
965963
) {
966-
await getOwnedKBOrThrow(kbId, ctx)
964+
await assertOwnedKB(kbId, ctx)
967965
await ctx.prisma.kBChatbot.deleteMany({ where: { kbId, chatbotId } })
968966
return await getOwnedKBOrThrow(kbId, ctx)
969967
}

packages/kb-management/src/components/KnowledgeBaseSettingsView.tsx

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
LinkedConsumer,
1212
UpdateKnowledgeBaseInput,
1313
} from '../types.js'
14-
import { getRefreshPolicyLabel } from '../utils.js'
14+
import { getRefreshPolicyLabel, safePrompt } from '../utils.js'
1515

1616
interface KnowledgeBaseSettingsViewProps {
1717
knowledgeBase?: KnowledgeBaseSummary
@@ -242,51 +242,35 @@ export function KnowledgeBaseSettingsView({
242242
</Panel>
243243
)}
244244

245-
{(onLinkCourse || onUnlinkCourse) && (
246-
<Panel
247-
title="Linked courses"
248-
description="Courses that use this knowledge base"
249-
>
250-
<LinkedConsumerList
251-
items={knowledgeBase.linkedCourses}
252-
promptLabel="Course ID"
253-
addLabel="Link course"
254-
onLink={
255-
onLinkCourse
256-
? (courseId) => onLinkCourse(knowledgeBase.id, courseId)
257-
: undefined
258-
}
259-
onUnlink={
260-
onUnlinkCourse
261-
? (courseId) => onUnlinkCourse(knowledgeBase.id, courseId)
262-
: undefined
263-
}
264-
/>
265-
</Panel>
266-
)}
245+
{LINKED_CONSUMER_PANELS.map((panel) => {
246+
const link = panel.kind === 'course' ? onLinkCourse : onLinkChatbot
247+
const unlink =
248+
panel.kind === 'course' ? onUnlinkCourse : onUnlinkChatbot
249+
if (!link && !unlink) return null
267250

268-
{(onLinkChatbot || onUnlinkChatbot) && (
269-
<Panel
270-
title="Linked chatbots"
271-
description="Chatbots that consume this knowledge base"
272-
>
273-
<LinkedConsumerList
274-
items={knowledgeBase.linkedChatbots}
275-
promptLabel="Chatbot ID"
276-
addLabel="Link chatbot"
277-
onLink={
278-
onLinkChatbot
279-
? (chatbotId) => onLinkChatbot(knowledgeBase.id, chatbotId)
280-
: undefined
281-
}
282-
onUnlink={
283-
onUnlinkChatbot
284-
? (chatbotId) => onUnlinkChatbot(knowledgeBase.id, chatbotId)
285-
: undefined
286-
}
287-
/>
288-
</Panel>
289-
)}
251+
const items =
252+
panel.kind === 'course'
253+
? knowledgeBase.linkedCourses
254+
: knowledgeBase.linkedChatbots
255+
256+
return (
257+
<Panel
258+
key={panel.kind}
259+
title={panel.title}
260+
description={panel.description}
261+
>
262+
<LinkedConsumerList
263+
items={items}
264+
promptLabel={panel.promptLabel}
265+
addLabel={panel.addLabel}
266+
onLink={link ? (id) => link(knowledgeBase.id, id) : undefined}
267+
onUnlink={
268+
unlink ? (id) => unlink(knowledgeBase.id, id) : undefined
269+
}
270+
/>
271+
</Panel>
272+
)
273+
})}
290274
</div>
291275
</div>
292276
)
@@ -352,6 +336,23 @@ function KnowledgeBaseDetailsForm({
352336
)
353337
}
354338

339+
const LINKED_CONSUMER_PANELS = [
340+
{
341+
kind: 'course' as const,
342+
title: 'Linked courses',
343+
description: 'Courses that use this knowledge base',
344+
promptLabel: 'Course ID',
345+
addLabel: 'Link course',
346+
},
347+
{
348+
kind: 'chatbot' as const,
349+
title: 'Linked chatbots',
350+
description: 'Chatbots that consume this knowledge base',
351+
promptLabel: 'Chatbot ID',
352+
addLabel: 'Link chatbot',
353+
},
354+
]
355+
355356
function LinkedConsumerList({
356357
items,
357358
promptLabel,
@@ -365,19 +366,18 @@ function LinkedConsumerList({
365366
onLink?: (id: string) => void
366367
onUnlink?: (id: string) => void
367368
}) {
369+
const list = items ?? []
368370
const handleAdd = () => {
369371
if (!onLink) return
370-
const id = typeof window === 'undefined' ? null : window.prompt(promptLabel)
372+
const id = safePrompt(promptLabel)
371373
if (!id) return
372374
onLink(id)
373375
}
374376

375377
return (
376378
<div className="space-y-2">
377-
{(items ?? []).length === 0 && (
378-
<EmptySetting>None linked yet.</EmptySetting>
379-
)}
380-
{(items ?? []).map((item) => (
379+
{list.length === 0 && <EmptySetting>None linked yet.</EmptySetting>}
380+
{list.map((item) => (
381381
<div
382382
key={item.id}
383383
className="flex items-center justify-between gap-2 rounded-md border border-slate-200 bg-white p-2 text-sm"

packages/kb-management/src/components/KnowledgeBaseSidebar.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
KnowledgeMetadataFieldDefinition,
88
UpdateKnowledgeBaseInput,
99
} from '../types.js'
10+
import { safeConfirm, safePrompt } from '../utils.js'
1011
import { MetadataChips } from './MetadataChips.js'
1112
import { StatusBadge } from './StatusBadge.js'
1213

@@ -36,32 +37,21 @@ export function KnowledgeBaseSidebar({
3637
}: KnowledgeBaseSidebarProps) {
3738
const handleCreate = () => {
3839
if (!onCreateKnowledgeBase) return
39-
const name =
40-
typeof window === 'undefined'
41-
? null
42-
: window.prompt('Knowledge base name')
40+
const name = safePrompt('Knowledge base name')
4341
if (!name) return
4442
onCreateKnowledgeBase({ name })
4543
}
4644

4745
const handleRename = (kb: KnowledgeBaseSummary) => {
4846
if (!onUpdateKnowledgeBase) return
49-
const name =
50-
typeof window === 'undefined'
51-
? null
52-
: window.prompt('Rename knowledge base', kb.name)
47+
const name = safePrompt('Rename knowledge base', kb.name)
5348
if (!name || name === kb.name) return
5449
onUpdateKnowledgeBase(kb.id, { name })
5550
}
5651

5752
const handleDelete = (kb: KnowledgeBaseSummary) => {
5853
if (!onDeleteKnowledgeBase) return
59-
if (
60-
typeof window !== 'undefined' &&
61-
!window.confirm(`Delete "${kb.name}"? This cannot be undone.`)
62-
) {
63-
return
64-
}
54+
if (!safeConfirm(`Delete "${kb.name}"? This cannot be undone.`)) return
6555
onDeleteKnowledgeBase(kb.id)
6656
}
6757
return (

packages/kb-management/src/components/ResourceInspector.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DEFAULT_RESOURCE_TYPES,
1414
getRefreshPolicyLabel,
1515
getResourceTypeDefinition,
16+
safePrompt,
1617
} from '../utils.js'
1718
import { ActivityPanels } from './ActivityPanels.js'
1819
import { MetadataChips } from './MetadataChips.js'
@@ -60,10 +61,7 @@ export function ResourceInspector({
6061
}: ResourceInspectorProps) {
6162
const handleRename = () => {
6263
if (!resource || !onUpdateResource) return
63-
const title =
64-
typeof window === 'undefined'
65-
? null
66-
: window.prompt('Resource title', resource.title)
64+
const title = safePrompt('Resource title', resource.title)
6765
if (!title || title === resource.title) return
6866
onUpdateResource(resource.id, { title })
6967
}

packages/kb-management/src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,19 @@ export function filterKnowledgeResources(
216216
})
217217
}
218218

219+
export function safePrompt(
220+
message: string,
221+
defaultValue?: string
222+
): string | null {
223+
if (typeof window === 'undefined') return null
224+
return window.prompt(message, defaultValue)
225+
}
226+
227+
export function safeConfirm(message: string): boolean {
228+
if (typeof window === 'undefined') return false
229+
return window.confirm(message)
230+
}
231+
219232
export function getRefreshPolicyLabel(policy?: KnowledgeRefreshPolicy) {
220233
if (!policy) return 'Manual'
221234
if (policy.label) return policy.label

0 commit comments

Comments
 (0)