Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic page metadata #570

Merged
merged 6 commits into from
Aug 20, 2024
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
35 changes: 12 additions & 23 deletions scripts/check-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ const fs = require('fs');
const path = require('path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const readline = require('readline');
const { resolveMetadata } = require('next/dist/lib/metadata/resolve-metadata');

const { generateCombinations, slugify } = require('./create-ai-assisted-dev-tools-comparison-pages');
const { tools } = require('../schema/data/ai-assisted-developer-tools.json');

const appDir = path.join(process.cwd(), 'src', 'app');
const expectedMetadataFields = ['title', 'description', 'openGraph', 'twitter', 'author', 'date', 'image'];

// Add a debug flag
const debug = process.argv.includes('--debug');

function analyzeFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
Expand All @@ -27,9 +22,7 @@ function analyzeFile(filePath) {
const metadataString = directMetadataMatch ? directMetadataMatch[1] : createMetadataMatch[1];
const definedFields = metadataString.match(/(\w+):/g).map(field => field.replace(':', ''));

// Check if createMetadata is used
if (createMetadataMatch) {
// Add fields that are always included in createMetadata
definedFields.push('openGraph', 'twitter');
}

Expand Down Expand Up @@ -60,7 +53,7 @@ function analyzeFile(filePath) {
ExportDeclaration(path) {
if (path.node.declaration && path.node.declaration.id && path.node.declaration.id.name === 'generateMetadata') {
hasMetadata = true;
definedFields = ['dynamic']; // Assume dynamic metadata covers all fields
definedFields = ['dynamic'];
}
},
});
Expand All @@ -76,19 +69,16 @@ function analyzeFile(filePath) {
function handleDynamicPages() {
const dynamicPages = [];

// Handle vector database comparison pages
const vectorDbDir = path.join(process.cwd(), 'src', 'app', 'comparisons', 'vector-databases');
dynamicPages.push(path.join(vectorDbDir, 'page.mdx'));

// Handle AI-assisted dev tools comparison pages
const combinations = generateCombinations(tools);
combinations.forEach(([tool1, tool2]) => {
const slug = `${slugify(tool1.name)}-vs-${slugify(tool2.name)}`;
const comparisonDir = path.join(process.cwd(), 'src', 'app', 'comparisons', slug);
dynamicPages.push(path.join(comparisonDir, 'page.mdx'));
});

// Handle the main AI-assisted dev tools comparison post
const mainComparisonDir = path.join(process.cwd(), 'src', 'app', 'blog', 'ai-assisted-dev-tools-compared');
dynamicPages.push(path.join(mainComparisonDir, 'page.mdx'));

Expand Down Expand Up @@ -120,7 +110,6 @@ function generateReport() {

traverseDir(appDir);

// Handle dynamic pages
const dynamicPages = handleDynamicPages();
dynamicPages.forEach(pagePath => {
analyzeAndAddToReport(pagePath, report);
Expand Down Expand Up @@ -187,7 +176,7 @@ function generatePRComment(report) {
comment += "No metadata issues found in this pull request. Great job!\n";
}

comment += "For full details, please check the [metadata-report.md](../artifacts/metadata-reports/metadata-report.md) artifact.\n\n";
comment += "For full details, please check the metadata-report.md artifact.\n\n";

comment += "## Artifacts\n\n";
comment += "* metadata-report.md\n";
Expand All @@ -196,6 +185,16 @@ function generatePRComment(report) {
return comment;
}

function writeReportAndLog(report) {
const markdownReport = generatePRComment(report);
fs.writeFileSync('metadata-report.md', markdownReport);

const jsonReport = JSON.stringify(report, null, 2);
fs.writeFileSync('metadata-report.json', jsonReport);

console.log(markdownReport);
}

async function debugMetadata(report) {
const rl = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -267,16 +266,6 @@ function parseMetadata(fileContent) {
return null;
}

function writeReportAndLog(report) {
const markdownReport = generatePRComment(report);
fs.writeFileSync('metadata-report.md', markdownReport);

const jsonReport = JSON.stringify(report, null, 2);
fs.writeFileSync('metadata-report.json', jsonReport);

console.log(markdownReport);
}

if (process.argv.includes('--debug')) {
const report = generateReport();
debugMetadata(report);
Expand Down
147 changes: 147 additions & 0 deletions src/app/chat/ChatPageClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use client'

import Link from 'next/link';
import { Container } from '@/components/Container';
import { useState, Suspense } from 'react';
import { useChat } from 'ai/react';
import { track } from '@vercel/analytics';
import { clsx } from 'clsx';
import RandomImage from '@/components/RandomImage';
import SearchForm from '@/components/SearchForm';
import { LoadingAnimation } from '@/components/LoadingAnimation';
import { BlogPostCard } from '@/components/BlogPostCard';
import { ArticleWithSlug } from '@/lib/shared-types';

const prepopulatedQuestions = [
"What is the programming bug?",
"Why do you love Next.js so much?",
"What do you do at Pinecone?",
"How can I become a better developer?",
"What is ggshield and why is it important?",
"How can I use AI to complete side projects more quickly?"
];

export default function ChatPageClient() {
const [isLoading, setIsLoading] = useState(false);
const [articles, setArticles] = useState<ArticleWithSlug[]>([]);

const { messages, input, setInput, handleSubmit } = useChat({
onResponse(response) {
const sourcesHeader = response.headers.get('x-sources');
const parsedArticles: ArticleWithSlug[] = sourcesHeader
? (JSON.parse(atob(sourcesHeader as string)) as ArticleWithSlug[])
: [];
console.log(`parsedArticle %o`, parsedArticles);
setArticles(parsedArticles);
setIsLoading(false);
},
headers: {},
onFinish() {
gtag("event", "chat_question", {
event_category: "chat",
event_label: input,
});
track("chat", { question: input });
},
onError() {
setIsLoading(false);
}
});

const handleSearch = async (query: string) => {
setInput(query);

gtag("event", "chat_use_precanned_question", {
event_category: "chat",
event_label: query,
});

track("chat-precanned", { question: query });

const customSubmitEvent = {
preventDefault: () => { },
} as unknown as React.FormEvent<HTMLFormElement>;

await handleSubmit(customSubmitEvent);
};

return (
<Container>
<div className="max-w-7xl mx-auto mt-16 sm:mt-32">
<div className="flex flex-col md:flex-row items-start mb-12">
<div className="flex-1 pl-8">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl mb-6">
Chat with me
</h1>
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4">
This experience uses Pinecone, OpenAI and LangChain...
</p>
<p className="prose dark:text-white">
Learn how to build this <Link href="/blog/langchain-pinecone-chat-with-my-blog" className="text-emerald-500 hover:text-emerald-600">with my tutorial</Link>
</p>
</div>
<div className="mt-6 md:mt-0 w-full md:w-80 h-80">
<Suspense fallback={<div className="w-full h-full bg-gray-200 rounded-lg pr-8 mr-8"></div>}>
<RandomImage />
</Suspense>
</div>
</div>

{/* Chat interface */}
<div className="mb-8">
<SearchForm
suggestedSearches={prepopulatedQuestions}
onSearch={handleSearch}
setIsLoading={setIsLoading}
/>
</div>

{isLoading && messages?.length > 0 && <LoadingAnimation />}

{/* Chat messages and related posts */}
<div className="flex flex-col md:flex-row">
<div className="flex-1 pr-0 md:pr-6 mb-6 md:mb-0">
{messages.map((m) => (
<div
key={m.id}
className="mb-4 whitespace-pre-wrap text-lg leading-relaxed"
>
<span
className={clsx('font-bold', {
'text-blue-700': m.role === 'user',
'text-green-700': m.role !== 'user',
})}
>
{m.role === 'user'
? 'You: '
: "The Ghost of Zachary Proser's Writing: "}
</span>
{m.content}
</div>
))}
</div>
<div className="md:w-1/3">
{Array.isArray(articles) && (articles.length > 0) && (
<div className="">
<h3 className="mb-4 text-xl font-semibold">Related Posts</h3>
<div className="space-y-4">
{(articles as ArticleWithSlug[]).map((article) => (
<BlogPostCard key={article.slug} article={article} />
))}
</div>
</div>
)}
</div>
</div>
<div className="mt-8 flex justify-end">
<button
onClick={() => { location.reload(); }}
className="px-4 py-2 bg-emerald-500 text-white rounded-lg hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-opacity-50"
>
Clear Chat
</button>
</div>
</div>
</Container>
);
}
Loading
Loading