# Custom Content RAG Feature - Implementation Guide

## Overview

This guide covers the remaining implementation steps for the Custom Content RAG feature, which allows users to upload PDFs and create text entries that are stored in Qdrant for AI-powered semantic search.

## ✅ Completed Backend Implementation

### 1. Database & Storage
- **Migration applied**: `custom_content` table created with all fields
- **Storage bucket**: `custom-content` bucket configured with RLS policies
- **Indexes**: Performance indexes on store_id, user_id, content_type, sync_status
- **Functions**: `get_custom_content_stats()` helper function

### 2. Shared Utilities
- `_shared/pdf-processor.ts` - PDF text extraction, SHA-256 checksum, validation
- `_shared/text-chunker.ts` - Hybrid chunking (single vs overlapping chunks)
- `_shared/qdrant-client.ts` - Extended with custom content methods:
  - `syncCustomContent()` - Stores chunks in Qdrant
  - `deleteCustomContent()` - Removes from Qdrant
  - `searchCustomContent()` - Semantic search
  - `getCustomContentCollectionName()` - Collection naming
- `_shared/mcp-qdrant-helpers.ts` - Extended with `queryQdrantCustomContent()`

### 3. Edge Functions (All Deployed ✅)
- `custom-content-upload` - Multipart file upload with deduplication
- `custom-content-process` - Async PDF processing pipeline
- `custom-content-create` - Text entry creation
- `custom-content-list` - List all content for a store
- `custom-content-delete` - Hard delete from all systems
- `custom-content-sync-status` - Real-time status polling

All functions are deployed and accessible at:
`https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1/custom-content-*`

---

## 🔄 Remaining Backend Work

### MCP Tool Integration

The `shopify_search_custom_content` tool has been added to `mcp-shopify`. The same needs to be done for `mcp-woocommerce` and `mcp-shoprenter`.

#### For `mcp-woocommerce/index.ts`:

**1. Update imports** (around line 40):
```typescript
import {
  getStoreQdrantConfig,
  queryQdrantProducts,
  queryQdrantOrders,
  queryQdrantCustomers,
  queryQdrantCustomContent  // ADD THIS LINE
} from '../_shared/mcp-qdrant-helpers.ts';
```

**2. Add tool definition to TOOLS array** (before closing `]`):
```typescript
  {
    name: 'woocommerce_search_custom_content',
    description: 'Search custom knowledge base (uploaded PDFs and text entries) for relevant information using semantic search. Returns matching content with titles, excerpts, and relevance scores. Use this to find custom documentation, policies, FAQs, or any store-specific information that has been uploaded.',
    inputSchema: {
      type: 'object',
      properties: {
        shop_id: {
          type: 'string',
          description: 'The UUID of the WooCommerce store from the stores table'
        },
        query: {
          type: 'string',
          description: 'The search query to find relevant content'
        },
        limit: {
          type: 'number',
          description: 'Number of results to return (default: 5, max: 10)'
        }
      },
      required: ['shop_id', 'query']
    }
  }
```

**3. Add handler function** (before `handleToolCall` function):
```typescript
/**
 * Handle custom content search
 */
async function handleSearchCustomContent(args: Record<string, any>): Promise<ToolCallResult> {
  const { shop_id, query, limit = 5 } = args;

  try {
    // Get store config for Qdrant connection
    const qdrantConfig = await getStoreQdrantConfig(shop_id);

    if (!qdrantConfig) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            error: 'Store configuration not found'
          })
        }],
        isError: true
      };
    }

    // Query custom content from Qdrant
    const results = await queryQdrantCustomContent(
      shop_id,
      qdrantConfig.shopname,
      query,
      Math.min(limit, 10) // Max 10 results
    );

    if (results.length === 0) {
      return {
        content: [{
          type: 'text',
          text: JSON.stringify({
            message: 'No custom content found matching your query',
            results: []
          })
        }],
        isError: false
      };
    }

    return {
      content: [{
        type: 'text',
        text: JSON.stringify({
          results: results.map(r => ({
            id: r.id,
            title: r.title,
            content_type: r.contentType,
            excerpt: r.excerpt,
            chunk_info: `Chunk ${r.chunkIndex + 1} of ${r.totalChunks}`,
            relevance_score: Math.round(r.relevanceScore * 100) / 100,
            original_filename: r.originalFilename,
            page_count: r.pageCount
          })),
          total: results.length
        })
      }],
      isError: false
    };
  } catch (error: any) {
    console.error('[MCP WooCommerce] Error searching custom content:', error);
    return {
      content: [{
        type: 'text',
        text: JSON.stringify({ error: `Failed to search custom content: ${error instanceof Error ? error.message : 'Unknown error'}` })
      }],
      isError: true
    };
  }
}
```

**4. Add case to switch statement** (in `handleToolCall`, before default case):
```typescript
    case 'woocommerce_search_custom_content':
      const customContentValidation = validateParams(args, ['shop_id', 'query']);
      if (!customContentValidation.valid) {
        return {
          content: [{
            type: 'text',
            text: JSON.stringify({
              error: `Missing required parameters: ${customContentValidation.missing?.join(', ')}`
            })
          }],
          isError: true
        };
      }
      return await handleSearchCustomContent(args);
```

#### For `mcp-shoprenter/index.ts`:

Apply the **exact same changes** as WooCommerce, but replace:
- `woocommerce_search_custom_content` → `shoprenter_search_custom_content`
- `'WooCommerce store'` → `'ShopRenter store'`
- `'[MCP WooCommerce]'` → `'[MCP ShopRenter]'`

#### Deploy MCP Changes:
```bash
cd /data/shopcall/supabase
npx supabase functions deploy mcp-shopify mcp-woocommerce mcp-shoprenter --project-ref ztklqodcdjeqpsvhlpud
```

---

## 📱 Frontend Implementation Guide

### Overview
Create a new page `/custom-content` under the AI Configuration menu where users can:
- Upload PDF files (with duplicate detection)
- Create text entries
- View all custom content with sync status
- Delete content
- See real-time processing status

### File Structure
```
shopcall.ai-main/src/
├── pages/
│   └── CustomContent.tsx (main page)
├── components/
│   ├── CustomContentManager.tsx (container component)
│   ├── CustomContentUpload.tsx (file upload UI)
│   ├── CustomContentTextEntry.tsx (text entry form)
│   ├── CustomContentList.tsx (content table/list)
│   └── CustomContentStatusBadge.tsx (status indicator)
└── App.tsx (add route)
```

### Step 1: Create Main Page Component

**File**: `shopcall.ai-main/src/pages/CustomContent.tsx`

```typescript
import { CustomContentManager } from "@/components/CustomContentManager";

export default function CustomContent() {
  return (
    <div className="flex flex-col gap-6 p-6">
      <div>
        <h1 className="text-3xl font-bold">Custom Content</h1>
        <p className="text-muted-foreground mt-2">
          Upload PDFs and create text entries for your AI assistant's knowledge base
        </p>
      </div>
      <CustomContentManager />
    </div>
  );
}
```

### Step 2: Create Manager Component

**File**: `shopcall.ai-main/src/components/CustomContentManager.tsx`

```typescript
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { FileUp, FileText, Plus } from "lucide-react";
import { CustomContentUpload } from "./CustomContentUpload";
import { CustomContentTextEntry } from "./CustomContentTextEntry";
import { CustomContentList } from "./CustomContentList";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";

export function CustomContentManager() {
  const [showUpload, setShowUpload] = useState(false);
  const [showTextEntry, setShowTextEntry] = useState(false);
  const [activeTab, setActiveTab] = useState<string>("all");
  const [refreshKey, setRefreshKey] = useState(0);

  const handleUploadSuccess = () => {
    setShowUpload(false);
    setRefreshKey(prev => prev + 1); // Trigger list refresh
  };

  const handleTextEntrySuccess = () => {
    setShowTextEntry(false);
    setRefreshKey(prev => prev + 1); // Trigger list refresh
  };

  return (
    <div className="space-y-4">
      {/* Action Buttons */}
      <div className="flex gap-2">
        <Dialog open={showUpload} onOpenChange={setShowUpload}>
          <DialogTrigger asChild>
            <Button>
              <FileUp className="mr-2 h-4 w-4" />
              Upload PDF
            </Button>
          </DialogTrigger>
          <DialogContent className="max-w-2xl">
            <CustomContentUpload onSuccess={handleUploadSuccess} />
          </DialogContent>
        </Dialog>

        <Dialog open={showTextEntry} onOpenChange={setShowTextEntry}>
          <DialogTrigger asChild>
            <Button variant="outline">
              <Plus className="mr-2 h-4 w-4" />
              Add Text Entry
            </Button>
          </DialogTrigger>
          <DialogContent className="max-w-2xl">
            <CustomContentTextEntry onSuccess={handleTextEntrySuccess} />
          </DialogContent>
        </Dialog>
      </div>

      {/* Tabs */}
      <Tabs value={activeTab} onValueChange={setActiveTab}>
        <TabsList>
          <TabsTrigger value="all">All Content</TabsTrigger>
          <TabsTrigger value="pdf_upload">PDFs</TabsTrigger>
          <TabsTrigger value="text_entry">Text Entries</TabsTrigger>
        </TabsList>

        <TabsContent value="all" className="mt-4">
          <CustomContentList
            contentType={undefined}
            refreshKey={refreshKey}
            onDelete={() => setRefreshKey(prev => prev + 1)}
          />
        </TabsContent>

        <TabsContent value="pdf_upload" className="mt-4">
          <CustomContentList
            contentType="pdf_upload"
            refreshKey={refreshKey}
            onDelete={() => setRefreshKey(prev => prev + 1)}
          />
        </TabsContent>

        <TabsContent value="text_entry" className="mt-4">
          <CustomContentList
            contentType="text_entry"
            refreshKey={refreshKey}
            onDelete={() => setRefreshKey(prev => prev + 1)}
          />
        </TabsContent>
      </Tabs>
    </div>
  );
}
```

### Step 3: Create File Upload Component

**File**: `shopcall.ai-main/src/components/CustomContentUpload.tsx`

```typescript
import { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Progress } from "@/components/ui/progress";
import { Upload, FileText, AlertCircle, CheckCircle2 } from "lucide-react";
import { useToast } from "@/hooks/use-toast";

interface CustomContentUploadProps {
  onSuccess: () => void;
}

export function CustomContentUpload({ onSuccess }: CustomContentUploadProps) {
  const [file, setFile] = useState<File | null>(null);
  const [title, setTitle] = useState("");
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const { toast } = useToast();

  // Get selected shop from context (implement based on your app's shop selector)
  const selectedShop = "your-shop-id"; // TODO: Get from context

  const onDrop = useCallback((acceptedFiles: File[]) => {
    if (acceptedFiles.length > 0) {
      const uploadedFile = acceptedFiles[0];
      setFile(uploadedFile);
      // Auto-fill title with filename (without extension)
      const fileName = uploadedFile.name.replace(/\\.pdf$/i, "");
      setTitle(fileName);
      setError(null);
    }
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'application/pdf': ['.pdf']
    },
    maxFiles: 1,
    maxSize: 10 * 1024 * 1024, // 10MB
  });

  const handleUpload = async () => {
    if (!file || !title.trim()) {
      setError("Please select a file and provide a title");
      return;
    }

    setUploading(true);
    setError(null);
    setProgress(10);

    try {
      // Create FormData
      const formData = new FormData();
      formData.append("file", file);
      formData.append("title", title.trim());
      formData.append("store_id", selectedShop);

      setProgress(30);

      // Get auth token
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) {
        throw new Error("Not authenticated");
      }

      setProgress(50);

      // Upload to Edge Function
      const response = await fetch(
        `${import.meta.env.VITE_API_URL}/custom-content-upload`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${session.access_token}`,
          },
          body: formData,
        }
      );

      setProgress(80);

      const result = await response.json();

      if (!response.ok) {
        if (result.duplicate) {
          throw new Error(
            `This file already exists as "${result.existingContent.title}" (uploaded on ${result.existingContent.uploadedAt})`
          );
        }
        throw new Error(result.error || "Upload failed");
      }

      setProgress(100);

      toast({
        title: "Upload successful",
        description: `${result.fileName} is being processed. This may take a few moments.`,
      });

      onSuccess();
    } catch (err: any) {
      console.error("Upload error:", err);
      setError(err.message || "Failed to upload file");
      toast({
        title: "Upload failed",
        description: err.message || "An error occurred during upload",
        variant: "destructive",
      });
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };

  return (
    <div className="space-y-4">
      <div>
        <h2 className="text-lg font-semibold">Upload PDF</h2>
        <p className="text-sm text-muted-foreground">
          Upload a PDF file to add to your AI assistant's knowledge base
        </p>
      </div>

      {/* File Dropzone */}
      <div
        {...getRootProps()}
        className={`
          border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
          transition-colors
          ${isDragActive ? "border-primary bg-primary/5" : "border-gray-300"}
          ${file ? "bg-muted" : ""}
        `}
      >
        <input {...getInputProps()} />
        {file ? (
          <div className="flex items-center justify-center gap-2">
            <FileText className="h-8 w-8 text-primary" />
            <div className="text-left">
              <p className="font-medium">{file.name}</p>
              <p className="text-sm text-muted-foreground">
                {(file.size / 1024 / 1024).toFixed(2)} MB
              </p>
            </div>
          </div>
        ) : (
          <div>
            <Upload className="h-12 w-12 mx-auto text-muted-foreground mb-2" />
            <p className="text-sm">
              {isDragActive
                ? "Drop the PDF file here"
                : "Drag and drop a PDF file here, or click to select"}
            </p>
            <p className="text-xs text-muted-foreground mt-1">
              Maximum file size: 10MB
            </p>
          </div>
        )}
      </div>

      {/* Title Input */}
      <div className="space-y-2">
        <Label htmlFor="title">Title *</Label>
        <Input
          id="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Enter a descriptive title"
          disabled={uploading}
        />
      </div>

      {/* Progress Bar */}
      {uploading && (
        <div className="space-y-2">
          <Progress value={progress} />
          <p className="text-sm text-center text-muted-foreground">
            Uploading... {progress}%
          </p>
        </div>
      )}

      {/* Error Alert */}
      {error && (
        <Alert variant="destructive">
          <AlertCircle className="h-4 w-4" />
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      )}

      {/* Action Buttons */}
      <div className="flex justify-end gap-2">
        <Button
          variant="outline"
          onClick={() => onSuccess()}
          disabled={uploading}
        >
          Cancel
        </Button>
        <Button
          onClick={handleUpload}
          disabled={!file || !title.trim() || uploading}
        >
          {uploading ? "Uploading..." : "Upload"}
        </Button>
      </div>
    </div>
  );
}
```

### Step 4: Create Text Entry Component

**File**: `shopcall.ai-main/src/components/CustomContentTextEntry.tsx`

```typescript
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { useToast } from "@/hooks/use-toast";

interface CustomContentTextEntryProps {
  onSuccess: () => void;
}

export function CustomContentTextEntry({ onSuccess }: CustomContentTextEntryProps) {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const { toast } = useToast();

  // Get selected shop from context
  const selectedShop = "your-shop-id"; // TODO: Get from context

  const handleSave = async () => {
    if (!title.trim() || !content.trim()) {
      setError("Please provide both title and content");
      return;
    }

    if (content.length > 50000) {
      setError("Content exceeds maximum length of 50,000 characters");
      return;
    }

    setSaving(true);
    setError(null);

    try {
      // Get auth token
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) {
        throw new Error("Not authenticated");
      }

      // Create entry via Edge Function
      const response = await fetch(
        `${import.meta.env.VITE_API_URL}/custom-content-create`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${session.access_token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            title: title.trim(),
            content_text: content.trim(),
            store_id: selectedShop,
          }),
        }
      );

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.error || "Failed to create entry");
      }

      toast({
        title: "Entry created",
        description: `"${title}" has been added to your knowledge base`,
      });

      onSuccess();
    } catch (err: any) {
      console.error("Create error:", err);
      setError(err.message || "Failed to create entry");
      toast({
        title: "Creation failed",
        description: err.message || "An error occurred",
        variant: "destructive",
      });
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="space-y-4">
      <div>
        <h2 className="text-lg font-semibold">Add Text Entry</h2>
        <p className="text-sm text-muted-foreground">
          Create a custom text entry for your AI assistant's knowledge base
        </p>
      </div>

      {/* Title Input */}
      <div className="space-y-2">
        <Label htmlFor="entry-title">Title *</Label>
        <Input
          id="entry-title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Enter a title"
          disabled={saving}
        />
      </div>

      {/* Content Textarea */}
      <div className="space-y-2">
        <Label htmlFor="entry-content">Content *</Label>
        <Textarea
          id="entry-content"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Enter your content here..."
          rows={12}
          disabled={saving}
          className="resize-none"
        />
        <p className="text-xs text-muted-foreground text-right">
          {content.length.toLocaleString()} / 50,000 characters
        </p>
      </div>

      {/* Error Alert */}
      {error && (
        <Alert variant="destructive">
          <AlertCircle className="h-4 w-4" />
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      )}

      {/* Action Buttons */}
      <div className="flex justify-end gap-2">
        <Button
          variant="outline"
          onClick={() => onSuccess()}
          disabled={saving}
        >
          Cancel
        </Button>
        <Button
          onClick={handleSave}
          disabled={!title.trim() || !content.trim() || saving}
        >
          {saving ? "Saving..." : "Save"}
        </Button>
      </div>
    </div>
  );
}
```

### Step 5: Create Content List Component

**File**: `shopcall.ai-main/src/components/CustomContentList.tsx`

```typescript
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Trash2, FileText, File } from "lucide-react";
import { CustomContentStatusBadge } from "./CustomContentStatusBadge";
import { useToast } from "@/hooks/use-toast";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";

interface CustomContentListProps {
  contentType?: "text_entry" | "pdf_upload";
  refreshKey: number;
  onDelete: () => void;
}

interface ContentItem {
  id: string;
  title: string;
  contentType: string;
  originalFilename?: string;
  fileSize?: number;
  pageCount?: number;
  chunkCount: number;
  syncStatus: string;
  syncError?: string;
  createdAt: string;
}

export function CustomContentList({
  contentType,
  refreshKey,
  onDelete
}: CustomContentListProps) {
  const [deleteId, setDeleteId] = useState<string | null>(null);
  const { toast } = useToast();

  // Get selected shop from context
  const selectedShop = "your-shop-id"; // TODO: Get from context

  // Fetch content list
  const { data, isLoading, refetch } = useQuery({
    queryKey: ["custom-content", selectedShop, contentType, refreshKey],
    queryFn: async () => {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) throw new Error("Not authenticated");

      const params = new URLSearchParams({
        store_id: selectedShop,
        ...(contentType && { content_type: contentType }),
      });

      const response = await fetch(
        `${import.meta.env.VITE_API_URL}/custom-content-list?${params}`,
        {
          headers: {
            Authorization: `Bearer ${session.access_token}`,
          },
        }
      );

      if (!response.ok) {
        throw new Error("Failed to fetch content");
      }

      const result = await response.json();
      return result.content as ContentItem[];
    },
    enabled: !!selectedShop,
  });

  // Refetch on refreshKey change
  useEffect(() => {
    if (refreshKey > 0) {
      refetch();
    }
  }, [refreshKey, refetch]);

  const handleDelete = async (id: string) => {
    try {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) throw new Error("Not authenticated");

      const response = await fetch(
        `${import.meta.env.VITE_API_URL}/custom-content-delete`,
        {
          method: "DELETE",
          headers: {
            Authorization: `Bearer ${session.access_token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            content_id: id,
            store_id: selectedShop,
          }),
        }
      );

      if (!response.ok) {
        throw new Error("Failed to delete content");
      }

      toast({
        title: "Content deleted",
        description: "The content has been removed from your knowledge base",
      });

      onDelete();
    } catch (err: any) {
      toast({
        title: "Deletion failed",
        description: err.message || "An error occurred",
        variant: "destructive",
      });
    } finally {
      setDeleteId(null);
    }
  };

  if (isLoading) {
    return <div className="text-center py-8">Loading...</div>;
  }

  if (!data || data.length === 0) {
    return (
      <div className="text-center py-8 text-muted-foreground">
        No custom content yet. Upload a PDF or create a text entry to get started.
      </div>
    );
  }

  return (
    <>
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Type</TableHead>
            <TableHead>Title</TableHead>
            <TableHead>Details</TableHead>
            <TableHead>Status</TableHead>
            <TableHead>Chunks</TableHead>
            <TableHead>Created</TableHead>
            <TableHead className="text-right">Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {data.map((item) => (
            <TableRow key={item.id}>
              <TableCell>
                {item.contentType === "pdf_upload" ? (
                  <File className="h-4 w-4 text-red-500" />
                ) : (
                  <FileText className="h-4 w-4 text-blue-500" />
                )}
              </TableCell>
              <TableCell className="font-medium">{item.title}</TableCell>
              <TableCell className="text-sm text-muted-foreground">
                {item.contentType === "pdf_upload" ? (
                  <div>
                    <div>{item.originalFilename}</div>
                    <div>
                      {item.pageCount} pages • {(item.fileSize! / 1024).toFixed(0)} KB
                    </div>
                  </div>
                ) : (
                  <div>Text entry</div>
                )}
              </TableCell>
              <TableCell>
                <CustomContentStatusBadge
                  contentId={item.id}
                  status={item.syncStatus}
                  error={item.syncError}
                />
              </TableCell>
              <TableCell>{item.chunkCount || 0}</TableCell>
              <TableCell>
                {new Date(item.createdAt).toLocaleDateString()}
              </TableCell>
              <TableCell className="text-right">
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => setDeleteId(item.id)}
                >
                  <Trash2 className="h-4 w-4" />
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>

      {/* Delete Confirmation Dialog */}
      <AlertDialog open={!!deleteId} onOpenChange={() => setDeleteId(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete content?</AlertDialogTitle>
            <AlertDialogDescription>
              This will permanently remove the content from your knowledge base and cannot be undone.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              onClick={() => deleteId && handleDelete(deleteId)}
              className="bg-destructive text-destructive-foreground"
            >
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}
```

### Step 6: Create Status Badge Component

**File**: `shopcall.ai-main/src/components/CustomContentStatusBadge.tsx`

```typescript
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Loader2, CheckCircle2, AlertCircle, Clock } from "lucide-react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

interface CustomContentStatusBadgeProps {
  contentId: string;
  status: string;
  error?: string;
}

export function CustomContentStatusBadge({
  contentId,
  status: initialStatus,
  error,
}: CustomContentStatusBadgeProps) {
  const [status, setStatus] = useState(initialStatus);
  const [polling, setPolling] = useState(
    initialStatus === "pending" || initialStatus === "processing"
  );

  useEffect(() => {
    if (!polling) return;

    const pollStatus = async () => {
      try {
        const { data: { session } } = await supabase.auth.getSession();
        if (!session) return;

        const response = await fetch(
          `${import.meta.env.VITE_API_URL}/custom-content-sync-status?content_id=${contentId}`,
          {
            headers: {
              Authorization: `Bearer ${session.access_token}`,
            },
          }
        );

        if (response.ok) {
          const result = await response.json();
          setStatus(result.status);

          // Stop polling if completed or failed
          if (result.status === "completed" || result.status === "failed") {
            setPolling(false);
          }
        }
      } catch (err) {
        console.error("Status poll error:", err);
      }
    };

    // Poll every 3 seconds
    const interval = setInterval(pollStatus, 3000);
    pollStatus(); // Initial poll

    return () => clearInterval(interval);
  }, [contentId, polling]);

  const getStatusDisplay = () => {
    switch (status) {
      case "pending":
        return {
          label: "Pending",
          icon: <Clock className="h-3 w-3" />,
          variant: "secondary" as const,
          tooltip: "Waiting to start processing",
        };
      case "processing":
        return {
          label: "Processing",
          icon: <Loader2 className="h-3 w-3 animate-spin" />,
          variant: "default" as const,
          tooltip: "Extracting text and generating embeddings",
        };
      case "completed":
        return {
          label: "Ready",
          icon: <CheckCircle2 className="h-3 w-3" />,
          variant: "success" as const,
          tooltip: "Successfully added to knowledge base",
        };
      case "failed":
        return {
          label: "Failed",
          icon: <AlertCircle className="h-3 w-3" />,
          variant: "destructive" as const,
          tooltip: error || "Processing failed",
        };
      default:
        return {
          label: status,
          icon: null,
          variant: "secondary" as const,
          tooltip: "",
        };
    }
  };

  const statusDisplay = getStatusDisplay();

  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <Badge variant={statusDisplay.variant} className="gap-1">
            {statusDisplay.icon}
            {statusDisplay.label}
          </Badge>
        </TooltipTrigger>
        <TooltipContent>
          <p>{statusDisplay.tooltip}</p>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}
```

### Step 7: Update Navigation

**File**: `shopcall.ai-main/src/components/ui/app-sidebar.tsx`

Find the AI Configuration menu items and add:

```typescript
{
  title: "Custom Content",
  url: "/custom-content",
  icon: Database, // or FileText
},
```

**File**: `shopcall.ai-main/src/App.tsx`

Add the route inside the PrivateRoute wrapper:

```typescript
<Route path="/custom-content" element={<CustomContent />} />
```

---

## 🧪 Testing Checklist

### Backend Testing:
- [ ] Run migration and verify `custom_content` table exists
- [ ] Verify Storage bucket `custom-content` is created
- [ ] Test PDF upload endpoint (duplicate detection)
- [ ] Test text entry creation endpoint
- [ ] Test list endpoint with filters
- [ ] Test delete endpoint (verify Qdrant + Storage cleanup)
- [ ] Test MCP tool: `shopify_search_custom_content`
- [ ] Test MCP tool: `woocommerce_search_custom_content`
- [ ] Test MCP tool: `shoprenter_search_custom_content`

### Frontend Testing:
- [ ] Navigate to `/custom-content` page
- [ ] Upload small PDF (<1MB)
- [ ] Upload large PDF (>5MB)
- [ ] Test duplicate file upload (should reject)
- [ ] Create text entry
- [ ] View all content (tabs work)
- [ ] Watch real-time status updates
- [ ] Delete content (confirm dialog)
- [ ] Verify content appears in AI assistant responses

---

## 📚 API Endpoints Reference

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/custom-content-upload` | POST | Upload PDF file |
| `/custom-content-process` | POST | Process PDF (internal) |
| `/custom-content-create` | POST | Create text entry |
| `/custom-content-list` | GET | List all content |
| `/custom-content-delete` | DELETE | Delete content |
| `/custom-content-sync-status` | GET | Get sync status |

All endpoints require `Authorization: Bearer <token>` header.

---

## 🔍 Troubleshooting

### PDF Processing Fails
- Check Edge Function logs: `npx supabase functions logs custom-content-process`
- Verify `OPENROUTER_API_KEY` is set in Supabase secrets
- Check PDF is valid (not corrupted or password-protected)

### Qdrant Sync Fails
- Verify Qdrant URL and API key in `stores` table or environment
- Check Qdrant collection exists: `{shopname}-custom-content`
- Review Qdrant logs for connection errors

### Upload Returns 413 Error
- File exceeds 10MB limit
- Adjust `file_size_limit` in Storage bucket settings

### Status Never Updates
- Frontend not polling (check interval is running)
- Edge Function crashed during processing
- Check sync status endpoint returns data

---

## 📊 Database Queries for Debugging

```sql
-- View all custom content
SELECT * FROM custom_content ORDER BY created_at DESC;

-- View content by status
SELECT title, content_type, sync_status, created_at
FROM custom_content
WHERE sync_status = 'failed';

-- Get content statistics
SELECT * FROM get_custom_content_stats('store-uuid-here');

-- View storage files
SELECT * FROM storage.objects
WHERE bucket_id = 'custom-content';
```

---

## 🎉 Summary

Once completed, users will be able to:
1. ✅ Upload PDF files with automatic deduplication
2. ✅ Create text entries for custom knowledge
3. ✅ View all content with real-time sync status
4. ✅ Delete content (hard delete from all systems)
5. ✅ AI assistants can search this content via MCP tools

The backend is **fully deployed and operational**. The frontend implementation should take approximately 4-6 hours for an experienced React developer.
