Files
wiki/convex/schema.ts

276 lines
12 KiB
TypeScript
Raw Permalink Normal View History

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
// Blog posts table
posts: defineTable({
slug: v.string(),
title: v.string(),
description: v.string(),
content: v.string(),
date: v.string(),
published: v.boolean(),
tags: v.array(v.string()),
readTime: v.optional(v.string()),
image: v.optional(v.string()), // Header/OG image URL
showImageAtTop: v.optional(v.boolean()), // Display image at top of post (default: false)
excerpt: v.optional(v.string()), // Short excerpt for card view
featured: v.optional(v.boolean()), // Show in featured section
featuredOrder: v.optional(v.number()), // Order in featured section (lower = first)
authorName: v.optional(v.string()), // Author display name
authorImage: v.optional(v.string()), // Author avatar image URL (round)
layout: v.optional(v.string()), // Layout type: "sidebar" for docs-style layout
rightSidebar: v.optional(v.boolean()), // Enable right sidebar with CopyPageDropdown
showFooter: v.optional(v.boolean()), // Show footer on this post (overrides siteConfig default)
footer: v.optional(v.string()), // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter: v.optional(v.boolean()), // Show social footer on this post (overrides siteConfig default)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: v.optional(v.boolean()), // Enable AI chat in right sidebar
blogFeatured: v.optional(v.boolean()), // Show as hero featured post on /blog page
newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false)
contactForm: v.optional(v.boolean()), // Enable contact form on this post
unlisted: v.optional(v.boolean()), // Hide from listings but allow direct access via slug
docsSection: v.optional(v.boolean()), // Include in docs navigation
docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs
docsSectionOrder: v.optional(v.number()), // Order within group (lower = first)
docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first)
docsSectionGroupIcon: v.optional(v.string()), // Phosphor icon name for sidebar group
docsLanding: v.optional(v.boolean()), // Use as /docs landing page
lastSyncedAt: v.number(),
source: v.optional(v.union(v.literal("dashboard"), v.literal("sync"))), // Content source: "dashboard" (created in UI) or "sync" (from markdown files)
embedding: v.optional(v.array(v.float64())), // Vector embedding for semantic search (1536 dimensions, OpenAI text-embedding-ada-002)
})
.index("by_slug", ["slug"])
.index("by_date", ["date"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
.index("by_blogFeatured", ["blogFeatured"])
.index("by_authorName", ["authorName"])
.index("by_docsSection", ["docsSection"])
.index("by_source", ["source"])
.searchIndex("search_content", {
searchField: "content",
filterFields: ["published"],
})
.searchIndex("search_title", {
searchField: "title",
filterFields: ["published"],
})
.vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536,
filterFields: ["published"],
}),
// Static pages (about, projects, contact, etc.)
pages: defineTable({
slug: v.string(),
title: v.string(),
content: v.string(),
published: v.boolean(),
order: v.optional(v.number()), // Display order in nav
showInNav: v.optional(v.boolean()), // Show in navigation menu (default: true)
excerpt: v.optional(v.string()), // Short excerpt for card view
image: v.optional(v.string()), // Thumbnail/OG image URL for featured cards
showImageAtTop: v.optional(v.boolean()), // Display image at top of page (default: false)
featured: v.optional(v.boolean()), // Show in featured section
featuredOrder: v.optional(v.number()), // Order in featured section (lower = first)
authorName: v.optional(v.string()), // Author display name
authorImage: v.optional(v.string()), // Author avatar image URL (round)
layout: v.optional(v.string()), // Layout type: "sidebar" for docs-style layout
rightSidebar: v.optional(v.boolean()), // Enable right sidebar with CopyPageDropdown
showFooter: v.optional(v.boolean()), // Show footer on this page (overrides siteConfig default)
footer: v.optional(v.string()), // Footer markdown content (overrides siteConfig defaultContent)
showSocialFooter: v.optional(v.boolean()), // Show social footer on this page (overrides siteConfig default)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: v.optional(v.boolean()), // Enable AI chat in right sidebar
contactForm: v.optional(v.boolean()), // Enable contact form on this page
newsletter: v.optional(v.boolean()), // Override newsletter signup display (true/false)
textAlign: v.optional(v.string()), // Text alignment: "left", "center", "right" (default: "left")
docsSection: v.optional(v.boolean()), // Include in docs navigation
docsSectionGroup: v.optional(v.string()), // Sidebar group name in docs
docsSectionOrder: v.optional(v.number()), // Order within group (lower = first)
docsSectionGroupOrder: v.optional(v.number()), // Order of group itself (lower = first)
docsSectionGroupIcon: v.optional(v.string()), // Phosphor icon name for sidebar group
docsLanding: v.optional(v.boolean()), // Use as /docs landing page
lastSyncedAt: v.number(),
source: v.optional(v.union(v.literal("dashboard"), v.literal("sync"))), // Content source: "dashboard" (created in UI) or "sync" (from markdown files)
embedding: v.optional(v.array(v.float64())), // Vector embedding for semantic search (1536 dimensions, OpenAI text-embedding-ada-002)
})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
.index("by_docsSection", ["docsSection"])
.index("by_source", ["source"])
.searchIndex("search_content", {
searchField: "content",
filterFields: ["published"],
})
.searchIndex("search_title", {
searchField: "title",
filterFields: ["published"],
})
.vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536,
filterFields: ["published"],
}),
// View counts for analytics
viewCounts: defineTable({
slug: v.string(),
count: v.number(),
}).index("by_slug", ["slug"]),
// Site configuration (about content, links, etc.)
siteConfig: defineTable({
key: v.string(),
value: v.any(),
}).index("by_key", ["key"]),
// Page view events for analytics (event records pattern)
pageViews: defineTable({
path: v.string(),
pageType: v.string(), // "blog" | "page" | "home" | "stats"
sessionId: v.string(),
timestamp: v.number(),
})
.index("by_path", ["path"])
.index("by_timestamp", ["timestamp"])
.index("by_session_path", ["sessionId", "path"]),
// Active sessions for real-time visitor tracking
activeSessions: defineTable({
sessionId: v.string(),
currentPath: v.string(),
lastSeen: v.number(),
// Location data (optional, from Netlify geo headers)
city: v.optional(v.string()),
country: v.optional(v.string()),
latitude: v.optional(v.number()),
longitude: v.optional(v.number()),
})
.index("by_sessionId", ["sessionId"])
.index("by_lastSeen", ["lastSeen"]),
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
// AI chat conversations for writing assistant
aiChats: defineTable({
sessionId: v.string(), // Anonymous session ID from localStorage
contextId: v.string(), // Slug or "write-page" identifier
messages: v.array(
v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
content: v.string(),
timestamp: v.number(),
attachments: v.optional(
v.array(
v.object({
type: v.union(v.literal("image"), v.literal("link")),
storageId: v.optional(v.id("_storage")),
url: v.optional(v.string()),
scrapedContent: v.optional(v.string()),
title: v.optional(v.string()),
}),
),
),
}),
),
pageContext: v.optional(v.string()), // Loaded page markdown content
lastMessageAt: v.optional(v.number()),
})
.index("by_session_and_context", ["sessionId", "contextId"])
.index("by_session", ["sessionId"]),
// AI generated images from Gemini image generation
aiGeneratedImages: defineTable({
sessionId: v.string(), // Anonymous session ID from localStorage
prompt: v.string(), // User's image prompt
model: v.string(), // Model used: "gemini-2.5-flash-image" or "gemini-3-pro-image-preview"
storageId: v.id("_storage"), // Convex storage ID for the generated image
mimeType: v.string(), // Image MIME type: "image/png" or "image/jpeg"
createdAt: v.number(), // Timestamp when image was generated
})
.index("by_session", ["sessionId"])
.index("by_createdAt", ["createdAt"]),
// Newsletter subscribers table
// Stores email subscriptions with unsubscribe tokens
newsletterSubscribers: defineTable({
email: v.string(), // Subscriber email address (lowercase, trimmed)
subscribed: v.boolean(), // Current subscription status
subscribedAt: v.number(), // Timestamp when subscribed
unsubscribedAt: v.optional(v.number()), // Timestamp when unsubscribed (if applicable)
source: v.string(), // Where they signed up: "home", "blog-page", "post", or "post:slug-name"
unsubscribeToken: v.string(), // Secure token for unsubscribe links
})
.index("by_email", ["email"])
.index("by_subscribed", ["subscribed"]),
// Newsletter sent tracking (posts and custom emails)
// Tracks what has been sent to prevent duplicate newsletters
newsletterSentPosts: defineTable({
postSlug: v.string(), // Slug of the post or custom email identifier
sentAt: v.number(), // Timestamp when the newsletter was sent
sentCount: v.number(), // Number of subscribers it was sent to
type: v.optional(v.string()), // "post" or "custom" (default "post" for backwards compat)
subject: v.optional(v.string()), // Subject line for custom emails
})
.index("by_postSlug", ["postSlug"])
.index("by_sentAt", ["sentAt"]),
// Contact form messages
// Stores messages submitted via contact forms on posts/pages
contactMessages: defineTable({
name: v.string(), // Sender's name
email: v.string(), // Sender's email address
message: v.string(), // Message content
source: v.string(), // Where submitted from: "page:slug" or "post:slug"
createdAt: v.number(), // Timestamp when submitted
emailSentAt: v.optional(v.number()), // Timestamp when email was sent (if applicable)
}).index("by_createdAt", ["createdAt"]),
// Ask AI sessions for header AI chat feature
// Stores questions and stream IDs for RAG-based Q&A
askAISessions: defineTable({
question: v.string(), // User's question
streamId: v.string(), // Persistent text streaming ID
model: v.optional(v.string()), // Selected AI model
createdAt: v.number(), // Timestamp when session was created
sources: v.optional(
v.array(
v.object({
title: v.string(),
slug: v.string(),
type: v.string(),
})
)
), // Optional sources cited in the response
}).index("by_stream", ["streamId"]),
// Content version history for posts and pages
// Stores snapshots before each update for 3-day retention
contentVersions: defineTable({
contentType: v.union(v.literal("post"), v.literal("page")), // Type of content
contentId: v.string(), // ID of the post or page (stored as string for flexibility)
slug: v.string(), // Slug for display and querying
title: v.string(), // Title at time of snapshot
content: v.string(), // Full markdown content at time of snapshot
description: v.optional(v.string()), // Description (posts only)
createdAt: v.number(), // Timestamp when version was created
source: v.union(
v.literal("sync"),
v.literal("dashboard"),
v.literal("restore")
), // What triggered the version capture
})
.index("by_content", ["contentType", "contentId"])
.index("by_slug", ["contentType", "slug"])
.index("by_createdAt", ["createdAt"])
.index("by_content_createdAt", ["contentType", "contentId", "createdAt"]),
// Version control settings
// Stores toggle state for version control feature
versionControlSettings: defineTable({
key: v.string(), // Setting key: "enabled"
value: v.boolean(), // Setting value
}).index("by_key", ["key"]),
});