Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
push:
branches:
- main
- develop
pull_request:

jobs:
test-backend:
name: Run Backend Tests
runs-on: ubuntu-latest
env: # <- job-level env
NEXT_PUBLIC_CONVEX_URL: "http://dummy-convex-url"
CONVEX_URL: "http://dummy-convex-url"
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Run backend tests
run: pnpm --filter backend test

test-frontend:
name: Run Frontend Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Run frontend tests
run: pnpm --filter web test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ node_modules
.env.local
.env.development.local
.env.test.local
.env.test
.env.production.local

# Testing
Expand Down Expand Up @@ -39,4 +40,5 @@ npm-debug.log*
# Other
*.md
*.txt
*.html
*.html

82 changes: 80 additions & 2 deletions apps/web/app/dashboard/configurations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ import {
import { BotSidebar } from "@/components/configurations/bot-sidebar";
import { Markdown } from "@/components/markdown";
import { KeyRound, Loader2, Check } from "lucide-react";
import { useState, useEffect } from "react";
import { useMemo, useState, useEffect } from "react";
import { KnowledgeBaseSection } from "@/components/configurations/knowledge-base-section";
import { useEnsureBotProfile, useGetBotConfig } from "@/lib/convex-client";
import { KBAnalytics } from "@/components/configurations/kb-analytics";
import {
useBotProfile,
useEnsureBotProfile,
useGetBotConfig,
useKnowledgeDocuments,
} from "@/lib/convex-client";
import type { Doc } from "@workspace/backend/convex/_generated/dataModel";
import { useAuth } from "@clerk/nextjs";
import { MODEL_CONFIG, type ModelId } from "@/lib/model-config";
import {
analyzeKBCompleteness,
generateKBInstructions,
} from "@/lib/system-prompt-builder";

export default function ConfigurationsPage() {
const { userId } = useAuth();
Expand Down Expand Up @@ -48,6 +58,28 @@ export default function ConfigurationsPage() {
// ===== BACKEND HOOKS =====
const ensureBotProfile = useEnsureBotProfile();
const botConfig = useGetBotConfig();
const botProfile = useBotProfile();
const knowledgeDocuments = useKnowledgeDocuments(botProfile?._id);

const kbDocsLoading = knowledgeDocuments === undefined;
const kbDocsForScore = useMemo(
() => knowledgeDocuments?.map((doc) => ({ text: doc.text || "" })) ?? [],
[knowledgeDocuments],
);
const kbCompleteness = useMemo(
() => analyzeKBCompleteness(systemPrompt || "", kbDocsForScore),
[systemPrompt, kbDocsForScore],
);
const kbInstructions = useMemo(
() => generateKBInstructions(kbDocsForScore.length),
[kbDocsForScore.length],
);
const kbScoreClass =
kbCompleteness.score >= 70
? "border-green-600/40 text-green-400 bg-green-900/20"
: kbCompleteness.score >= 50
? "border-yellow-600/40 text-yellow-400 bg-yellow-900/20"
: "border-red-600/40 text-red-400 bg-red-900/20";

// Ensure a bot profile exists for this user before interacting with config
useEffect(() => {
Expand Down Expand Up @@ -330,6 +362,50 @@ export default function ConfigurationsPage() {
"No system instructions defined."
)}
</div>

<div className="rounded-md border border-zinc-800 bg-zinc-900/40 p-3 text-xs text-muted-foreground space-y-2">
{kbDocsLoading ? (
<p className="text-xs text-muted-foreground">
Loading knowledge base insights...
</p>
) : (
<>
<div className="flex items-center justify-between">
<span>KB completeness</span>
<span
className={`text-[11px] px-2 py-0.5 rounded-full border ${kbScoreClass}`}
>
{kbCompleteness.score}%
</span>
</div>
<p className="text-xs text-muted-foreground">
{kbInstructions}
</p>
{kbCompleteness.warnings.length > 0 && (
<div className="space-y-1">
<p className="text-[11px] text-red-400">Warnings</p>
<ul className="list-disc pl-4 space-y-1">
{kbCompleteness.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
</div>
)}
{kbCompleteness.suggestions.length > 0 && (
<div className="space-y-1">
<p className="text-[11px] text-yellow-400">
Suggestions
</p>
<ul className="list-disc pl-4 space-y-1">
{kbCompleteness.suggestions.map((suggestion) => (
<li key={suggestion}>{suggestion}</li>
))}
</ul>
</div>
)}
</>
)}
</div>
</section>

{/* Escalation Settings - Structured Lead Capture */}
Expand Down Expand Up @@ -390,6 +466,8 @@ export default function ConfigurationsPage() {
isSelected={selectedKbSection}
onSelectSection={handleSelectKnowledgeBaseSection}
/>

<KBAnalytics botId={botProfile?._id} />
</TabsContent>

{/* ===== ADVANCED TAB ===== */}
Expand Down
14 changes: 1 addition & 13 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import { Geist, Geist_Mono } from "next/font/google";

import "@workspace/ui/globals.css";
import { Providers } from "@/components/providers";

const fontSans = Geist({
subsets: ["latin"],
variable: "--font-sans",
});

const fontMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
});

export default function RootLayout({
children,
}: Readonly<{
Expand All @@ -22,7 +10,7 @@ export default function RootLayout({
<html lang="en" suppressHydrationWarning>
<body
suppressHydrationWarning
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased `}
className="font-sans antialiased"
>
<Providers>{children}</Providers>
</body>
Expand Down
149 changes: 149 additions & 0 deletions apps/web/components/configurations/kb-analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { useMemo } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import { Badge } from "@workspace/ui/components/badge";
import { useKBStats, useKnowledgeDocuments } from "@/lib/convex-client";
import type { Id } from "@workspace/backend/convex/_generated/dataModel";
import { extractTitleFromContent } from "@/lib/kb-utils";

interface KBAnalyticsProps {
botId?: Id<"botProfiles"> | "skip";
days?: number;
}

type KBStats = {
totalDocuments: number;
documentsUsedLastPeriod: number;
totalRetrievals: number;
hitRate: number;
topDocuments: {
documentId: string;
count: number;
lastUsedAt: number;
}[];
unusedDocumentIds: string[];
windowDays: number;
};

export function KBAnalytics({ botId, days = 7 }: KBAnalyticsProps) {
const stats = useKBStats(botId, days) as KBStats | undefined;
const documents = useKnowledgeDocuments(botId);

const docTitleMap = useMemo(() => {
if (!documents) return new Map<string, string>();
return new Map(
documents.map((doc) => [
String(doc.id),
extractTitleFromContent(doc.text || "Untitled"),
]),
);
}, [documents]);

if (!botId || botId === "skip") {
return null;
}

if (!stats || !documents) {
return (
<Card className="border-zinc-800 bg-card">
<CardHeader>
<CardTitle className="text-base">Knowledge Base Analytics</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">Loading analytics...</p>
</CardContent>
</Card>
);
}

return (
<Card className="border-zinc-800 bg-card">
<CardHeader className="space-y-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Knowledge Base Analytics</CardTitle>
<Badge variant="outline" className="text-[11px]">
Last {stats.windowDays} days
</Badge>
</div>
<p className="text-xs text-muted-foreground">
Track how often your knowledge base appears in answers.
</p>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 p-3">
<p className="text-xs text-muted-foreground">Total docs</p>
<p className="text-lg font-semibold text-zinc-100">
{stats.totalDocuments}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 p-3">
<p className="text-xs text-muted-foreground">Docs used</p>
<p className="text-lg font-semibold text-zinc-100">
{stats.documentsUsedLastPeriod}
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 p-3">
<p className="text-xs text-muted-foreground">Hit rate</p>
<p className="text-lg font-semibold text-zinc-100">
{stats.hitRate}%
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 p-3">
<p className="text-xs text-muted-foreground">Retrievals</p>
<p className="text-lg font-semibold text-zinc-100">
{stats.totalRetrievals}
</p>
</div>
</div>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-3">
<p className="text-xs font-semibold text-zinc-200 mb-2">
Most used documents
</p>
{stats.topDocuments.length === 0 ? (
<p className="text-xs text-muted-foreground">
No document retrievals yet.
</p>
) : (
<ul className="space-y-2 text-xs text-muted-foreground">
{stats.topDocuments.map((doc) => (
<li key={doc.documentId} className="flex justify-between">
<span className="text-zinc-200">
{docTitleMap.get(doc.documentId) || "Untitled document"}
</span>
<span>{doc.count} uses</span>
</li>
))}
</ul>
)}
</div>

<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-3">
<p className="text-xs font-semibold text-zinc-200 mb-2">
Never used documents
</p>
{stats.unusedDocumentIds.length === 0 ? (
<p className="text-xs text-muted-foreground">
All documents were retrieved recently.
</p>
) : (
<ul className="space-y-2 text-xs text-muted-foreground">
{stats.unusedDocumentIds.slice(0, 5).map((id) => (
<li key={id}>{docTitleMap.get(id) || "Untitled document"}</li>
))}
</ul>
)}
</div>
</div>
</CardContent>
</Card>
);
}
Loading
Loading