Skip to content
This repository was archived by the owner on Apr 20, 2026. It is now read-only.
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
33 changes: 17 additions & 16 deletions src/app/api/docs/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { pool } from "@/lib/db/client";
import { NextRequest, NextResponse } from "next/server";
import type { Document, LinkedItem } from "@/lib/mock-docs";
import type { Document, LinkedItem } from "@/lib/docs/model";
import fs from "fs";
import path from "path";
import os from "os";
Expand Down Expand Up @@ -63,7 +63,7 @@ function isRepoDoc(filename: string): boolean {
return REPO_DOC_PATTERNS.some((pattern) => pattern.test(filename));
}

function guessAgent(filename: string, content: string): string {
function guessAgent(filename: string): string {
const fn = filename.toLowerCase();
if (fn.includes("cpars") || fn.includes("seas") || fn.includes("skyward")) return "skylar";
if (fn.includes("mbe") || fn.includes("cert") || fn.includes("wosb") || fn.includes("lsbrp")) return "veronica";
Expand Down Expand Up @@ -137,7 +137,7 @@ function readWorkspaceDocs(): Document[] {
stat = fs.statSync(filePath);
} catch { /* ignore */ }

const agentId = guessAgent(f, content);
const agentId = guessAgent(f);
const meta = AGENT_META[agentId] || AGENT_META.bob;

return {
Expand All @@ -164,6 +164,7 @@ function readWorkspaceDocs(): Document[] {
}
}

/** Get documents from best available source for local/dev runtime */
/** Non-database fallback: workspace documents only */
function getFallbackDocs(): Document[] {
return readWorkspaceDocs();
Expand Down Expand Up @@ -194,23 +195,24 @@ async function ensureSchema() {
}

export async function GET(request: NextRequest) {
if (!pool) return NextResponse.json(getFallbackDocs());
if (!pool) {
return NextResponse.json(getFallbackDocs());
}

try {
await ensureSchema();
} catch {
return NextResponse.json(getFallbackDocs());
return NextResponse.json({ error: "Unable to initialize docs schema" }, { status: 500 });
}

const { searchParams } = new URL(request.url);
const docType = searchParams.get("type");
const search = searchParams.get("search");

try {
// Use minimal columns to avoid schema drift (linked_to, etc. may not exist)
const baseQuery =
"SELECT d.id, d.title, d.filename, d.doc_type, d.content, d.author_agent_id, " +
"d.status, d.file_path, d.created_at, d.updated_at " +
"d.status, d.file_path, d.linked_to, d.version_history, d.priority, d.review_status, d.category, d.notes, d.assignments, d.created_at, d.updated_at " +
"FROM docs d";
let query = baseQuery;
const conditions: string[] = [];
Expand All @@ -224,7 +226,6 @@ export async function GET(request: NextRequest) {
params.push("%" + search + "%");
conditions.push("(d.title ILIKE $" + params.length + " OR d.content ILIKE $" + params.length + ")");
}
// Skip linkedType, reviewStatus, category, priority filters - those columns may not exist in deployed schema

if (conditions.length > 0) {
query += " WHERE " + conditions.join(" AND ");
Expand All @@ -248,20 +249,20 @@ export async function GET(request: NextRequest) {
updatedAt: row.updated_at,
agent: meta.name,
agentEmoji: meta.emoji,
linkedTo: [] as LinkedItem[],
versionHistory: [{ timestamp: row.updated_at as string, summary: "Synced" }],
priority: "medium",
reviewStatus: "pending_review",
category: "uncategorized",
notes: [],
assignments: [],
linkedTo: (row.linked_to as LinkedItem[]) || [],
versionHistory: (row.version_history as { timestamp: string; summary: string }[]) || [],
priority: (row.priority as string) || "medium",
reviewStatus: (row.review_status as string) || "pending_review",
category: (row.category as string) || "uncategorized",
notes: (row.notes as unknown[]) || [],
assignments: (row.assignments as unknown[]) || [],
};
});

return NextResponse.json(docs);
} catch (error) {
console.error("[Docs API] Error:", error);
return NextResponse.json(getFallbackDocs());
return NextResponse.json({ error: "Failed to fetch documents" }, { status: 500 });
}
}

Expand Down
46 changes: 40 additions & 6 deletions src/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type {
LinkedItem,
AssignTarget,
DocumentPriority,
} from "@/lib/mock-docs";
import { CATEGORY_OPTIONS } from "@/lib/mock-docs";
} from "@/lib/docs/model";
import { CATEGORY_OPTIONS } from "@/lib/docs/model";
import DocCard from "@/components/docs/DocCard";
import DocViewer from "@/components/docs/DocViewer";
import DocCreateModal from "@/components/docs/DocCreateModal";
Expand All @@ -24,6 +24,7 @@ type TabMode = "all" | "queue";
export default function DocsPage() {
const [documents, setDocuments] = useState<Document[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
Expand Down Expand Up @@ -64,19 +65,32 @@ export default function DocsPage() {
const fetchDocuments = useCallback(async () => {
try {
setLoading(true);
setLoadError(null);
setError(null);

const res = await fetch("/api/docs", { cache: "no-store" });
const data = await res.json().catch(() => null);

if (!res.ok) {
throw new Error((data && (data.error as string)) || "Failed to load documents");
const message = (data && typeof data.error === "string" && data.error) || "Failed to load documents.";
setLoadError(message);
setError(message);
return;
}

if (!Array.isArray(data)) {
throw new Error("Unexpected documents response");
const message = "Unexpected response while loading documents.";
setLoadError(message);
setError(message);
return;
}

setDocuments(data);
setLastUpdated(new Date().toISOString());
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load documents");
} catch {
const message = "Unable to reach docs service. Check your connection and try again.";
setLoadError(message);
setError(message);
} finally {
setLoading(false);
}
Expand Down Expand Up @@ -375,6 +389,12 @@ export default function DocsPage() {
</div>
</div>

{loadError && (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-sm text-red-300">
{loadError}
</div>
)}

{/* Tabs: Queue vs All */}
<div className="flex items-center gap-1 mb-6 bg-gray-900 border border-gray-800 rounded-lg p-0.5 w-fit">
<button
Expand Down Expand Up @@ -508,6 +528,10 @@ export default function DocsPage() {
<div className="text-center py-12">
<p className="text-gray-500 text-sm">Loading documents...</p>
</div>
) : loadError && documents.length === 0 ? (
<div className="text-center py-12">
<p className="text-red-300 text-sm">Could not load review queue.</p>
</div>
) : (
<ReviewQueue
documents={filteredDocs}
Expand All @@ -521,6 +545,16 @@ export default function DocsPage() {
<div className="text-center py-12">
<p className="text-gray-500 text-sm">Loading documents...</p>
</div>
) : loadError && documents.length === 0 ? (
<div className="text-center py-12">
<p className="text-red-300 text-sm mb-3">Unable to load documents.</p>
<button
onClick={() => fetchDocuments()}
className="px-4 py-2 bg-red-600/20 text-red-300 border border-red-500/30 rounded-lg text-sm font-medium hover:bg-red-600/30 transition-colors"
>
Retry
</button>
</div>
) : filteredDocs.length === 0 ? (
<div className="text-center py-16">
<div className="text-4xl mb-3 opacity-30">&#128196;</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/docs/DocAssignModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useState, useEffect } from "react";
import type { Document, AssignTarget, DocumentPriority } from "@/lib/mock-docs";
import { PRIORITY_OPTIONS } from "@/lib/mock-docs";
import type { Document, AssignTarget, DocumentPriority } from "@/lib/docs/model";
import { PRIORITY_OPTIONS } from "@/lib/docs/model";
import { useAgentStore } from "@/lib/stores/agentStore";

interface Pipeline {
Expand Down
4 changes: 2 additions & 2 deletions src/components/docs/DocCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"use client";

import type { Document, DocumentStatus, DocumentType, DocumentPriority, ReviewStatus, LinkedItem } from "@/lib/mock-docs";
import type { Document, DocumentStatus, DocumentType, DocumentPriority, ReviewStatus, LinkedItem } from "@/lib/docs/model";
import { linkTypeConfig } from "@/components/docs/LinkPicker";
import { priorityStyles, reviewStatusStyles, categoryStyles } from "@/lib/ui-config";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/mock-docs";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/docs/model";
import { getRelativeTime, getWordCount } from "@/lib/utils/formatting";

interface DocCardProps {
Expand Down
2 changes: 1 addition & 1 deletion src/components/docs/DocCreateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import { DocumentType, LinkedItem } from "@/lib/mock-docs";
import { DocumentType, LinkedItem } from "@/lib/docs/model";
import LinkPicker from "@/components/docs/LinkPicker";
import { useAgentStore } from "@/lib/stores/agentStore";

Expand Down
2 changes: 1 addition & 1 deletion src/components/docs/DocNotes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState } from "react";
import type { DocNote } from "@/lib/mock-docs";
import type { DocNote } from "@/lib/docs/model";

interface DocNotesProps {
docId: string;
Expand Down
4 changes: 2 additions & 2 deletions src/components/docs/DocViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useState } from "react";
import type { Document, DocumentStatus, DocumentPriority, ReviewStatus, DocumentCategory, LinkedItem } from "@/lib/mock-docs";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/mock-docs";
import type { Document, DocumentStatus, DocumentPriority, ReviewStatus, DocumentCategory, LinkedItem } from "@/lib/docs/model";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/docs/model";
import MarkdownRenderer from "@/components/chat/MarkdownRenderer";
import LinkPicker, { linkTypeConfig } from "@/components/docs/LinkPicker";
import DocNotes from "@/components/docs/DocNotes";
Expand Down
2 changes: 1 addition & 1 deletion src/components/docs/LinkPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import type { LinkedItem } from "@/lib/mock-docs";
import type { LinkedItem } from "@/lib/docs/model";

interface LinkPickerProps {
linkedItems: LinkedItem[];
Expand Down
4 changes: 2 additions & 2 deletions src/components/docs/ReviewQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type {
DocumentPriority,
ReviewStatus,
DocumentCategory,
} from "@/lib/mock-docs";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/mock-docs";
} from "@/lib/docs/model";
import { PRIORITY_OPTIONS, REVIEW_STATUS_OPTIONS, CATEGORY_OPTIONS } from "@/lib/docs/model";
import { priorityStyles, reviewStatusStyles, categoryStyles } from "@/lib/ui-config";
import { getRelativeTime } from "@/lib/utils/formatting";

Expand Down
99 changes: 99 additions & 0 deletions src/lib/docs/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export type DocumentType =
| "proposal"
| "capability_statement"
| "certification_doc"
| "report"
| "template";

export type DocumentStatus = "draft" | "in_review" | "approved" | "exported";

export type DocumentPriority = "critical" | "high" | "medium" | "low";

export type ReviewStatus = "pending_review" | "reviewed" | "needs_changes" | "approved" | "rejected";

export type DocumentCategory =
| "govcon"
| "internal"
| "compliance"
| "financial"
| "technical"
| "hr"
| "marketing"
| "legal"
| "uncategorized";

export type AssignTarget = "memory" | "task" | "orchestration";

export interface LinkedItem {
type: "deal" | "certification" | "task";
id: string;
name: string;
}

export interface VersionEntry {
timestamp: string;
summary: string;
}

export interface DocNote {
id: string;
author: string;
content: string;
createdAt: string;
}

export interface DocAssignment {
target: AssignTarget;
targetId?: string;
agentId?: string;
instructions?: string;
priority?: DocumentPriority;
assignedAt: string;
status: "pending" | "in_progress" | "completed" | "failed";
}

export interface Document {
id: string;
title: string;
type: DocumentType;
agent: string;
agentEmoji: string;
status: DocumentStatus;
content: string;
linkedTo?: LinkedItem[];
createdAt: string;
updatedAt: string;
versionHistory?: VersionEntry[];
priority?: DocumentPriority;
reviewStatus?: ReviewStatus;
category?: DocumentCategory;
notes?: DocNote[];
assignments?: DocAssignment[];
}

export const CATEGORY_OPTIONS: { value: DocumentCategory; label: string }[] = [
{ value: "govcon", label: "GovCon" },
{ value: "internal", label: "Internal" },
{ value: "compliance", label: "Compliance" },
{ value: "financial", label: "Financial" },
{ value: "technical", label: "Technical" },
{ value: "hr", label: "HR" },
{ value: "marketing", label: "Marketing" },
{ value: "legal", label: "Legal" },
{ value: "uncategorized", label: "Uncategorized" },
];

export const PRIORITY_OPTIONS: { value: DocumentPriority; label: string; color: string }[] = [
{ value: "critical", label: "Critical", color: "text-red-400" },
{ value: "high", label: "High", color: "text-orange-400" },
{ value: "medium", label: "Medium", color: "text-yellow-400" },
{ value: "low", label: "Low", color: "text-gray-400" },
];

export const REVIEW_STATUS_OPTIONS: { value: ReviewStatus; label: string }[] = [
{ value: "pending_review", label: "Pending Review" },
{ value: "reviewed", label: "Reviewed" },
{ value: "needs_changes", label: "Needs Changes" },
{ value: "approved", label: "Approved" },
{ value: "rejected", label: "Rejected" },
];
2 changes: 1 addition & 1 deletion src/lib/ui-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DocumentPriority, ReviewStatus } from "@/lib/mock-docs";
import type { DocumentPriority, ReviewStatus } from "@/lib/docs/model";

export const priorityStyles: Record<
DocumentPriority,
Expand Down