All files / web/src/app/api/flowcharts/browse route.ts

0% Statements 0/164
0% Branches 0/1
0% Functions 0/1
0% Lines 0/164

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165                                                                                                                                                                                                                                                                                                                                         
import { desc, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/withAuth'
import { db, schema } from '@/db'
import { getUserId } from '@/lib/viewer'
import { getAllFlowchartEmbeddings, cosineSimilarity } from '@/lib/flowcharts/embedding-search'
import { loadTaxonomy, labelId } from '@/lib/flowcharts/taxonomy'

/**
 * GET /api/flowcharts/browse
 * List all published flowcharts from the database.
 *
 * NOTE: Built-in flowcharts must be seeded via the Seed Manager
 * (debug mode on /flowchart) to appear in this list.
 *
 * Query params:
 * - difficulty: 'Beginner' | 'Intermediate' | 'Advanced' (optional filter)
 * - search: string (optional search term)
 * - limit: number (optional, defaults to 50)
 * - offset: number (optional, defaults to 0)
 *
 * Returns: { flowcharts: FlowchartMeta[], total: number }
 */
export const GET = withAuth(async (request) => {
  try {
    const url = new URL(request.url)
    const difficulty = url.searchParams.get('difficulty') as
      | 'Beginner'
      | 'Intermediate'
      | 'Advanced'
      | null
    const search = url.searchParams.get('search')
    const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100)
    const offset = parseInt(url.searchParams.get('offset') || '0')

    // Get published flowcharts from database only
    const dbFlowcharts = await db.query.teacherFlowcharts.findMany({
      where: eq(schema.teacherFlowcharts.status, 'published'),
      orderBy: [desc(schema.teacherFlowcharts.publishedAt)],
      columns: {
        id: true,
        title: true,
        description: true,
        emoji: true,
        difficulty: true,
        userId: true,
        publishedAt: true,
        searchKeywords: true,
      },
    })

    // Transform to response format
    let allFlowcharts = dbFlowcharts.map((fc) => ({
      id: fc.id,
      title: fc.title,
      description: fc.description || '',
      emoji: fc.emoji || '📊',
      difficulty: fc.difficulty as 'Beginner' | 'Intermediate' | 'Advanced',
      source: 'database' as const,
      authorId: fc.userId,
      publishedAt: fc.publishedAt,
      searchKeywords: fc.searchKeywords,
    }))

    // Apply difficulty filter
    if (difficulty) {
      allFlowcharts = allFlowcharts.filter((fc) => fc.difficulty === difficulty)
    }

    // Apply search filter
    if (search) {
      const searchLower = search.toLowerCase()
      allFlowcharts = allFlowcharts.filter((fc) => {
        const inTitle = fc.title.toLowerCase().includes(searchLower)
        const inDescription = fc.description?.toLowerCase().includes(searchLower)
        const inKeywords =
          'searchKeywords' in fc && fc.searchKeywords?.toLowerCase().includes(searchLower)
        return inTitle || inDescription || inKeywords
      })
    }

    const total = allFlowcharts.length

    // Apply pagination
    const paginatedFlowcharts = allFlowcharts.slice(offset, offset + limit)

    // Get current user ID for ownership checks (optional - doesn't fail if not logged in)
    let currentUserId: string | null = null
    try {
      currentUserId = await getUserId()
    } catch {
      // Not logged in, that's fine
    }

    // Compute pairwise cosine distance matrix for flowcharts with embeddings
    let distances: { ids: string[]; matrix: number[] } | undefined
    let labelBreadths: Record<string, number> | undefined
    try {
      const embeddingCache = await getAllFlowchartEmbeddings()

      // Annotate each flowchart with whether it has an embedding
      for (const fc of paginatedFlowcharts) {
        ;(fc as Record<string, unknown>).hasEmbedding = embeddingCache.has(fc.id)
      }

      // Collect IDs of paginated flowcharts that have embeddings (content embedding)
      const idsWithEmbeddings: string[] = []
      const embeddings: Float32Array[] = []
      for (const fc of paginatedFlowcharts) {
        const cached = embeddingCache.get(fc.id)
        if (cached) {
          idsWithEmbeddings.push(fc.id)
          embeddings.push(cached.embedding)
        }
      }

      if (idsWithEmbeddings.length >= 2) {
        // Load taxonomy labels and include them in the distance matrix
        const allIds = [...idsWithEmbeddings]
        const allEmbeddings = [...embeddings]

        try {
          const taxonomy = await loadTaxonomy()
          // Build label breadths map for client-side use
          labelBreadths = {}
          for (let li = 0; li < taxonomy.labels.length; li++) {
            allIds.push(labelId(taxonomy.labels[li]))
            allEmbeddings.push(taxonomy.embeddings[li])
            labelBreadths[taxonomy.labels[li]] = taxonomy.breadths[li]
          }
        } catch (err) {
          // Non-fatal: taxonomy is optional (files may not exist yet)
          console.warn('Failed to load taxonomy:', err)
        }

        // Build upper-triangle flat distance array: d(0,1), d(0,2), ..., d(n-2,n-1)
        const n = allIds.length
        const matrix: number[] = []
        for (let i = 0; i < n; i++) {
          for (let j = i + 1; j < n; j++) {
            matrix.push(1 - cosineSimilarity(allEmbeddings[i], allEmbeddings[j]))
          }
        }
        distances = { ids: allIds, matrix }
      }
    } catch (err) {
      // Non-fatal: distances are optional
      console.error('Failed to compute distance matrix:', err)
    }

    return NextResponse.json({
      flowcharts: paginatedFlowcharts,
      total,
      limit,
      offset,
      currentUserId,
      distances,
      labelBreadths,
    })
  } catch (error) {
    console.error('Failed to browse flowcharts:', error)
    return NextResponse.json({ error: 'Failed to browse flowcharts' }, { status: 500 })
  }
})