[dyad] Implemented headline tree structure - wrote 4 file(s), deleted 1 file(s)
This commit is contained in:
@@ -7,10 +7,12 @@ interface FaqItem {
|
|||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeadlineItem {
|
export interface HeadlineNode {
|
||||||
tag: string;
|
tag: string;
|
||||||
text: string;
|
text: string;
|
||||||
length: number;
|
length: number;
|
||||||
|
level: number;
|
||||||
|
children: HeadlineNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function extractMetaData(url: string) {
|
export async function extractMetaData(url: string) {
|
||||||
@@ -79,17 +81,35 @@ export async function extractMetaData(url: string) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const headlines: HeadlineItem[] = [];
|
const headlines: HeadlineNode[] = [];
|
||||||
|
const path: HeadlineNode[] = [];
|
||||||
|
|
||||||
$("h1, h2, h3, h4, h5, h6").each((i, el) => {
|
$("h1, h2, h3, h4, h5, h6").each((i, el) => {
|
||||||
const tag = $(el).prop("tagName").toLowerCase();
|
const tag = $(el).prop("tagName").toLowerCase();
|
||||||
const text = $(el).text().trim();
|
const text = $(el).text().trim();
|
||||||
if (text) {
|
if (!text) return;
|
||||||
headlines.push({
|
|
||||||
|
const level = parseInt(tag.replace("h", ""), 10);
|
||||||
|
|
||||||
|
const node: HeadlineNode = {
|
||||||
tag,
|
tag,
|
||||||
text,
|
text,
|
||||||
length: text.length,
|
length: text.length,
|
||||||
});
|
level,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
while (path.length > 0 && path[path.length - 1].level >= level) {
|
||||||
|
path.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path.length === 0) {
|
||||||
|
headlines.push(node);
|
||||||
|
} else {
|
||||||
|
path[path.length - 1].children.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
53
src/components/headline-tree.tsx
Normal file
53
src/components/headline-tree.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import type { HeadlineNode } from "@/app/actions";
|
||||||
|
|
||||||
|
interface HeadlineTreeProps {
|
||||||
|
headlines: HeadlineNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeadlineNodeDisplay = ({
|
||||||
|
node,
|
||||||
|
level,
|
||||||
|
}: {
|
||||||
|
node: HeadlineNode;
|
||||||
|
level: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 py-2.5 border-b"
|
||||||
|
style={{ paddingLeft: `${level * 24}px` }}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="uppercase w-12 flex-shrink-0 justify-center font-mono"
|
||||||
|
>
|
||||||
|
{node.tag}
|
||||||
|
</Badge>
|
||||||
|
<p className="font-medium flex-grow text-foreground">{node.text}</p>
|
||||||
|
<p className="text-sm text-muted-foreground flex-shrink-0 w-10 text-right">
|
||||||
|
{node.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{node.children && node.children.length > 0 && (
|
||||||
|
<div>
|
||||||
|
{node.children.map((child, index) => (
|
||||||
|
<HeadlineNodeDisplay key={index} node={child} level={level + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HeadlineTree({ headlines }: HeadlineTreeProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-full border rounded-lg overflow-hidden">
|
||||||
|
{headlines.map((headline, index) => (
|
||||||
|
<HeadlineNodeDisplay key={index} node={headline} level={0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
interface HeadlinesDisplayProps {
|
|
||||||
headlines: {
|
|
||||||
tag: string;
|
|
||||||
text: string;
|
|
||||||
length: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeadlinesDisplay({ headlines }: HeadlinesDisplayProps) {
|
|
||||||
return (
|
|
||||||
<div className="border rounded-lg max-h-80 overflow-y-auto relative">
|
|
||||||
<Table>
|
|
||||||
<TableHeader className="sticky top-0 bg-muted/95 backdrop-blur-sm">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[80px]">Tag</TableHead>
|
|
||||||
<TableHead>Text</TableHead>
|
|
||||||
<TableHead className="text-right w-[100px]">Length</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{headlines.map((headline, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="secondary" className="uppercase">
|
|
||||||
{headline.tag}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">{headline.text}</TableCell>
|
|
||||||
<TableCell className="text-right">{headline.length}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,20 +6,20 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Globe, Edit, Check, Loader2, X, ImageOff } from "lucide-react";
|
import { Globe, Edit, Check, Loader2, X, ImageOff } from "lucide-react";
|
||||||
import { extractMetaData } from "@/app/actions";
|
import { extractMetaData, type HeadlineNode } from "@/app/actions";
|
||||||
import { LengthIndicator } from "./length-indicator";
|
import { LengthIndicator } from "./length-indicator";
|
||||||
import { CopyButton } from "./copy-button";
|
import { CopyButton } from "./copy-button";
|
||||||
import { SerpPreview } from "./serp-preview";
|
import { SerpPreview } from "./serp-preview";
|
||||||
import { ResultsSkeleton } from "./results-skeleton";
|
import { ResultsSkeleton } from "./results-skeleton";
|
||||||
import { FaqDisplay } from "./faq-display";
|
import { FaqDisplay } from "./faq-display";
|
||||||
import { HeadlinesDisplay } from "./headlines-display";
|
import { HeadlineTree } from "./headline-tree";
|
||||||
|
|
||||||
interface MetaData {
|
interface MetaData {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
faq?: { question: string; answer: string }[] | null;
|
faq?: { question: string; answer: string }[] | null;
|
||||||
headlines?: { tag: string; text: string; length: number }[] | null;
|
headlines?: HeadlineNode[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetaForm() {
|
export function MetaForm() {
|
||||||
@@ -283,11 +283,11 @@ export function MetaForm() {
|
|||||||
<Card className="w-full shadow-lg rounded-lg">
|
<Card className="w-full shadow-lg rounded-lg">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl text-card-foreground">
|
<CardTitle className="text-xl text-card-foreground">
|
||||||
Headlines
|
Headlines Structure
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<HeadlinesDisplay headlines={metaData.headlines} />
|
<HeadlineTree headlines={metaData.headlines} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,13 +61,17 @@ export function ResultsSkeleton() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Skeleton className="h-6 w-32" />
|
<Skeleton className="h-6 w-40" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="border rounded-lg p-2 space-y-2">
|
<div className="border rounded-lg p-2 space-y-2">
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-full" />
|
||||||
<Skeleton className="h-8 w-full" />
|
<div className="ml-6">
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-[90%]" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-6">
|
||||||
|
<Skeleton className="h-8 w-[90%]" />
|
||||||
|
</div>
|
||||||
<Skeleton className="h-8 w-full" />
|
<Skeleton className="h-8 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user