[dyad] Added status indicators to tabs - wrote 4 file(s)

This commit is contained in:
[dyad]
2026-01-20 14:37:17 +01:00
parent 41f8c0e5a1
commit e40c494e0a
4 changed files with 147 additions and 12 deletions

View File

@@ -7,6 +7,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { getLengthIndicatorColor } from "@/lib/analysis";
interface LengthIndicatorProps {
length: number;
@@ -24,20 +25,24 @@ const DESCRIPTION_THRESHOLDS = {
};
export function LengthIndicator({ length, type }: LengthIndicatorProps) {
const color = getLengthIndicatorColor(length, type);
const colorClassMap = {
green: "bg-green-500",
yellow: "bg-yellow-500",
red: "bg-red-500",
gray: "bg-gray-400",
};
const thresholds =
type === "title" ? TITLE_THRESHOLDS : DESCRIPTION_THRESHOLDS;
let colorClass = "bg-red-500";
let tooltipText = "Length is not optimal";
if (length === 0) {
tooltipText = "Not found";
colorClass = "bg-gray-400";
} else if (length >= thresholds.good.min && length <= thresholds.good.max) {
colorClass = "bg-green-500";
tooltipText = "Optimal length";
} else if (length >= thresholds.ok.min && length <= thresholds.ok.max) {
colorClass = "bg-yellow-500";
tooltipText = "Length could be improved";
} else if (length < thresholds.ok.min) {
tooltipText = "Too short";
@@ -49,7 +54,7 @@ export function LengthIndicator({ length, type }: LengthIndicatorProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={cn("h-3 w-3 rounded-full", colorClass)} />
<div className={cn("h-3 w-3 rounded-full", colorClassMap[color])} />
</TooltipTrigger>
<TooltipContent>
<p>

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -14,7 +14,11 @@ import {
ImageOff,
Search,
} from "lucide-react";
import { extractMetaData, type HeadlineNode, type ImageAltData } from "@/app/actions";
import {
extractMetaData,
type HeadlineNode,
type ImageAltData,
} from "@/app/actions";
import { LengthIndicator } from "./length-indicator";
import { CopyButton } from "./copy-button";
import { SerpPreview } from "./serp-preview";
@@ -23,6 +27,8 @@ import { FaqDisplay } from "./faq-display";
import { HeadlineTree } from "./headline-tree";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ImageAltDisplay } from "./image-alt-display";
import { TabIndicator } from "./tab-indicator";
import { getLengthIndicatorColor, type IndicatorColor } from "@/lib/analysis";
interface MetaData {
title: string;
@@ -57,6 +63,62 @@ export function MetaForm() {
}
}, [metaData]);
const tabColors = useMemo(() => {
if (!metaData) return null;
// Analysis Tab
const titleStatus = getLengthIndicatorColor(editableTitle.length, "title");
const descStatus = getLengthIndicatorColor(
editableDescription.length,
"description"
);
const analysisStatuses: IndicatorColor[] = [titleStatus, descStatus];
let analysisColor: IndicatorColor;
if (
analysisStatuses.includes("red") ||
analysisStatuses.includes("gray")
) {
analysisColor = "red";
} else if (analysisStatuses.includes("yellow")) {
analysisColor = "yellow";
} else {
analysisColor = "green";
}
// Headlines Tab
let headlinesColor: IndicatorColor = "gray";
if (metaData.headlines && metaData.headlines.length > 0) {
const h1s = metaData.headlines.filter((h) => h.tag === "h1");
if (h1s.length === 0) {
headlinesColor = "red";
} else if (h1s.length > 1) {
headlinesColor = "yellow";
} else {
headlinesColor = "green";
}
}
// Images Tab
let imagesColor: IndicatorColor = "gray";
if (metaData.images && metaData.images.length > 0) {
const missingAltCount = metaData.images.filter((img) => !img.alt).length;
imagesColor = missingAltCount > 0 ? "red" : "green";
}
// FAQ Tab
let faqColor: IndicatorColor = "gray";
if (metaData.faq && metaData.faq.length > 0) {
faqColor = "green";
}
return {
analysis: analysisColor,
headlines: headlinesColor,
images: imagesColor,
faq: faqColor,
};
}, [metaData, editableTitle, editableDescription]);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
@@ -149,15 +211,27 @@ export function MetaForm() {
{!loading && metaData && (
<Tabs defaultValue="analysis" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="analysis">Meta Analysis</TabsTrigger>
<TabsTrigger value="analysis">
{tabColors && <TabIndicator color={tabColors.analysis} />}
Meta Analysis
</TabsTrigger>
{metaData.headlines && metaData.headlines.length > 0 && (
<TabsTrigger value="headlines">Headlines</TabsTrigger>
<TabsTrigger value="headlines">
{tabColors && <TabIndicator color={tabColors.headlines} />}
Headlines
</TabsTrigger>
)}
{metaData.images && metaData.images.length > 0 && (
<TabsTrigger value="images">Images</TabsTrigger>
<TabsTrigger value="images">
{tabColors && <TabIndicator color={tabColors.images} />}
Images
</TabsTrigger>
)}
{metaData.faq && metaData.faq.length > 0 && (
<TabsTrigger value="faq">FAQ</TabsTrigger>
<TabsTrigger value="faq">
{tabColors && <TabIndicator color={tabColors.faq} />}
FAQ
</TabsTrigger>
)}
</TabsList>

View File

@@ -0,0 +1,26 @@
"use client";
import { cn } from "@/lib/utils";
import type { IndicatorColor } from "@/lib/analysis";
interface TabIndicatorProps {
color: IndicatorColor;
}
export function TabIndicator({ color }: TabIndicatorProps) {
const colorClassMap = {
green: "bg-green-500",
yellow: "bg-yellow-500",
red: "bg-red-500",
gray: "bg-gray-400",
};
return (
<div
className={cn(
"h-2 w-2 rounded-full mr-2 flex-shrink-0",
colorClassMap[color]
)}
/>
);
}

30
src/lib/analysis.ts Normal file
View File

@@ -0,0 +1,30 @@
export type IndicatorColor = "green" | "yellow" | "red" | "gray";
const TITLE_THRESHOLDS = {
good: { min: 30, max: 60 },
ok: { min: 15, max: 70 },
};
const DESCRIPTION_THRESHOLDS = {
good: { min: 120, max: 158 },
ok: { min: 50, max: 170 },
};
export function getLengthIndicatorColor(
length: number,
type: "title" | "description"
): IndicatorColor {
const thresholds =
type === "title" ? TITLE_THRESHOLDS : DESCRIPTION_THRESHOLDS;
if (length === 0) {
return "gray";
}
if (length >= thresholds.good.min && length <= thresholds.good.max) {
return "green";
}
if (length >= thresholds.ok.min && length <= thresholds.ok.max) {
return "yellow";
}
return "red";
}