chore(messages): update AI features in multiple language files

This commit is contained in:
Fatih Kadir Akın
2025-12-14 03:36:09 +03:00
parent 24e951f318
commit 7e55decf64
23 changed files with 504 additions and 130 deletions
+4 -1
View File
@@ -32,7 +32,10 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production"
# DO_SPACES_SECRET_ACCESS_KEY=""
# DO_SPACES_CDN_ENDPOINT="" # Optional: for CDN-enabled Spaces
# AI Search (optional - enable aiSearch in prompts.config.ts)
# AI Features (optional - enable aiSearch/aiGeneration in prompts.config.ts)
# OPENAI_API_KEY=your_openai_api_key
# OPENAI_BASE_URL=https://api.openai.com/v1 # Optional: custom base URL for OpenAI-compatible APIs
# OPENAI_EMBEDDING_MODEL=text-embedding-3-small # Optional: embedding model for AI search
# OPENAI_GENERATIVE_MODEL=gpt-4o-mini # Optional: generative model for AI generation
# GOOGLE_ANALYTICS_ID="G-XXXXXXXXX"
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "استعلام SQL",
"openDataset": "فتح مجموعة البيانات",
"runQuery": "تشغيل الاستعلام على HF",
"examples": "استعلامات نموذجية"
"examples": "أمثلة",
"selectExample": "اختر مثالاً...",
"aiGenerate": "توليد AI",
"aiPlaceholder": "صف استعلام SQL الذي تحتاجه..."
},
"edit": "تعديل الأمر",
"delete": "حذف الأمر",
@@ -509,7 +512,8 @@
"search": "بحث",
"clear": "مسح",
"found": "تم العثور على {count}",
"aiSearch": "بحث الذكاء الاصطناعي"
"aiSearch": "بحث الذكاء الاصطناعي",
"searchTags": "البحث عن العلامات..."
},
"user": {
"profile": "الملف الشخصي",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQL-Abfrage",
"openDataset": "Datensatz öffnen",
"runQuery": "Abfrage auf HF ausführen",
"examples": "Beispielabfragen"
"examples": "Beispiele",
"selectExample": "Beispiel auswählen...",
"aiGenerate": "KI Generieren",
"aiPlaceholder": "Beschreiben Sie die benötigte SQL-Abfrage..."
},
"edit": "Prompt bearbeiten",
"delete": "Prompt löschen",
@@ -509,7 +512,8 @@
"search": "Suchen",
"clear": "Löschen",
"found": "{count} gefunden",
"aiSearch": "KI-Suche"
"aiSearch": "KI-Suche",
"searchTags": "Tags suchen..."
},
"user": {
"profile": "Profil",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQL Query",
"openDataset": "Open Dataset",
"runQuery": "Run Query on HF",
"examples": "Example Queries"
"examples": "Examples",
"selectExample": "Select an example...",
"aiGenerate": "AI Generate",
"aiPlaceholder": "Describe the SQL query you need..."
},
"edit": "Edit Prompt",
"delete": "Delete Prompt",
@@ -509,7 +512,8 @@
"search": "Search",
"clear": "Clear",
"found": "{count} found",
"aiSearch": "AI Search"
"aiSearch": "AI Search",
"searchTags": "Search tags..."
},
"user": {
"profile": "Profile",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "Consulta SQL",
"openDataset": "Abrir Dataset",
"runQuery": "Ejecutar Consulta en HF",
"examples": "Consultas de Ejemplo"
"examples": "Ejemplos",
"selectExample": "Selecciona un ejemplo...",
"aiGenerate": "Generar con IA",
"aiPlaceholder": "Describe la consulta SQL que necesitas..."
},
"edit": "Editar Prompt",
"delete": "Eliminar Prompt",
@@ -509,7 +512,8 @@
"search": "Buscar",
"clear": "Limpiar",
"found": "{count} encontrados",
"aiSearch": "Búsqueda IA"
"aiSearch": "Búsqueda IA",
"searchTags": "Buscar etiquetas..."
},
"user": {
"profile": "Perfil",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "Requête SQL",
"openDataset": "Ouvrir le Dataset",
"runQuery": "Exécuter la Requête sur HF",
"examples": "Exemples de Requêtes"
"examples": "Exemples",
"selectExample": "Sélectionner un exemple...",
"aiGenerate": "Générer IA",
"aiPlaceholder": "Décrivez la requête SQL dont vous avez besoin..."
},
"edit": "Modifier le Prompt",
"delete": "Supprimer le Prompt",
@@ -509,7 +512,8 @@
"search": "Rechercher",
"clear": "Effacer",
"found": "{count} trouvés",
"aiSearch": "Recherche IA"
"aiSearch": "Recherche IA",
"searchTags": "Rechercher des tags..."
},
"user": {
"profile": "Profil",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "Query SQL",
"openDataset": "Apri Dataset",
"runQuery": "Esegui Query su HF",
"examples": "Query di Esempio"
"examples": "Esempi",
"selectExample": "Seleziona un esempio...",
"aiGenerate": "Genera AI",
"aiPlaceholder": "Descrivi la query SQL di cui hai bisogno..."
},
"edit": "Modifica Prompt",
"delete": "Elimina Prompt",
@@ -509,7 +512,8 @@
"search": "Cerca",
"clear": "Cancella",
"found": "{count} trovati",
"aiSearch": "Ricerca AI"
"aiSearch": "Ricerca AI",
"searchTags": "Cerca tag..."
},
"user": {
"profile": "Profilo",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQLクエリ",
"openDataset": "データセットを開く",
"runQuery": "HFでクエリを実行",
"examples": "クエリ例"
"examples": "例",
"selectExample": "例を選択...",
"aiGenerate": "AI生成",
"aiPlaceholder": "必要なSQLクエリを説明してください..."
},
"edit": "プロンプトを編集",
"delete": "プロンプトを削除",
@@ -509,7 +512,8 @@
"search": "検索",
"clear": "クリア",
"found": "{count}件見つかりました",
"aiSearch": "AI検索"
"aiSearch": "AI検索",
"searchTags": "タグを検索..."
},
"user": {
"profile": "プロフィール",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQL 쿼리",
"openDataset": "데이터셋 열기",
"runQuery": "HF에서 쿼리 실행",
"examples": "예제 쿼리"
"examples": "예제",
"selectExample": "예제 선택...",
"aiGenerate": "AI 생성",
"aiPlaceholder": "필요한 SQL 쿼리를 설명하세요..."
},
"edit": "프롬프트 편집",
"delete": "프롬프트 삭제",
@@ -509,7 +512,8 @@
"search": "검색",
"clear": "지우기",
"found": "{count}개 찾음",
"aiSearch": "AI 검색"
"aiSearch": "AI 검색",
"searchTags": "태그 검색..."
},
"user": {
"profile": "프로필",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "Consulta SQL",
"openDataset": "Abrir Dataset",
"runQuery": "Executar Consulta no HF",
"examples": "Consultas de Exemplo"
"examples": "Exemplos",
"selectExample": "Selecione um exemplo...",
"aiGenerate": "Gerar com IA",
"aiPlaceholder": "Descreva a consulta SQL que você precisa..."
},
"edit": "Editar Prompt",
"delete": "Excluir Prompt",
@@ -509,7 +512,8 @@
"search": "Buscar",
"clear": "Limpar",
"found": "{count} encontrados",
"aiSearch": "Busca por IA"
"aiSearch": "Busca por IA",
"searchTags": "Pesquisar tags..."
},
"user": {
"profile": "Perfil",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQL Sorgusu",
"openDataset": "Veri Setini Aç",
"runQuery": "HF'de Sorguyu Çalıştır",
"examples": "Örnek Sorgular"
"examples": "Örnekler",
"selectExample": "Bir örnek seçin...",
"aiGenerate": "AI Oluştur",
"aiPlaceholder": "Nasıl bir prompt bulmak istiyorsunuz?"
},
"edit": "Prompt Düzenle",
"delete": "Prompt Sil",
@@ -509,7 +512,8 @@
"search": "Ara",
"clear": "Temizle",
"found": "{count} bulundu",
"aiSearch": "AI ile Ara"
"aiSearch": "AI ile Ara",
"searchTags": "Etiket ara..."
},
"user": {
"profile": "Profil",
+6 -2
View File
@@ -79,7 +79,10 @@
"sqlQuery": "SQL 查询",
"openDataset": "打开数据集",
"runQuery": "在HF上运行查询",
"examples": "示例查询"
"examples": "示例",
"selectExample": "选择一个示例...",
"aiGenerate": "AI生成",
"aiPlaceholder": "描述您需要的SQL查询..."
},
"edit": "编辑提示词",
"delete": "删除提示词",
@@ -509,7 +512,8 @@
"search": "搜索",
"clear": "清除",
"found": "找到 {count} 个",
"aiSearch": "AI 搜索"
"aiSearch": "AI 搜索",
"searchTags": "搜索标签..."
},
"user": {
"profile": "个人资料",
+3 -1
View File
@@ -59,7 +59,9 @@ export default defineConfig({
// Enable tags
tags: true,
// Enable AI-powered semantic search (requires OPENAI_API_KEY)
aiSearch: false,
aiSearch: true,
// Enable AI-powered generation features (requires OPENAI_API_KEY)
aiGeneration: true,
},
// Homepage customization
+34
View File
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { generateSQL, isAIGenerationEnabled } from "@/lib/ai/generation";
export async function POST(request: NextRequest) {
try {
const enabled = await isAIGenerationEnabled();
if (!enabled) {
return NextResponse.json(
{ error: "AI Generation is not enabled" },
{ status: 400 }
);
}
const body = await request.json();
const { prompt } = body;
if (!prompt || prompt.trim().length === 0) {
return NextResponse.json(
{ error: "Prompt is required" },
{ status: 400 }
);
}
const sql = await generateSQL(prompt);
return NextResponse.json({ sql });
} catch (error) {
console.error("SQL Generation error:", error);
return NextResponse.json(
{ error: "Failed to generate SQL" },
{ status: 500 }
);
}
}
+63 -23
View File
@@ -1,37 +1,77 @@
import { Skeleton } from "@/components/ui/skeleton";
function PromptCardSkeleton() {
return (
<div className="border rounded-lg p-4 space-y-3">
<div className="flex items-start justify-between">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-5 w-5 rounded" />
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-2/3" />
<div className="flex items-center gap-2 pt-2">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="h-4 w-24" />
</div>
<div className="flex gap-2">
<Skeleton className="h-5 w-14 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
</div>
);
}
export default function PromptsLoading() {
return (
<div className="container py-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<Skeleton className="h-6 w-32 mb-2" />
<Skeleton className="h-4 w-48" />
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<div className="flex items-baseline gap-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-4 w-16" />
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-8 w-28" />
</div>
<Skeleton className="h-9 w-28" />
</div>
{/* Filters */}
<div className="flex flex-wrap gap-2 mb-6">
<Skeleton className="h-9 w-64" />
<Skeleton className="h-9 w-32" />
<Skeleton className="h-9 w-32" />
</div>
{/* Prompt Grid */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
<div className="flex gap-2 pt-2">
<Skeleton className="h-5 w-16" />
<Skeleton className="h-5 w-16" />
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar Filters Skeleton */}
<aside className="w-full lg:w-56 shrink-0">
<div className="space-y-4 p-4 border rounded-lg">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-6 w-12" />
</div>
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-9 rounded-full" />
<Skeleton className="h-4 w-20" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-12" />
<div className="flex flex-wrap gap-1">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-6 w-16 rounded-full" />
))}
</div>
</div>
</div>
))}
</aside>
{/* Main Content - Prompt Grid Skeleton */}
<main className="flex-1 min-w-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 12 }).map((_, i) => (
<PromptCardSkeleton key={i} />
))}
</div>
</main>
</div>
</div>
);
+29 -24
View File
@@ -5,9 +5,11 @@ import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { InfinitePromptList } from "@/components/prompts/infinite-prompt-list";
import { PromptFilters } from "@/components/prompts/prompt-filters";
import { FilterProvider } from "@/components/prompts/filter-context";
import { HFDataStudioDropdown } from "@/components/prompts/hf-data-studio-dropdown";
import { db } from "@/lib/db";
import { isAISearchEnabled, semanticSearch } from "@/lib/ai/embeddings";
import { isAIGenerationEnabled } from "@/lib/ai/generation";
import config from "@/../prompts.config";
export const metadata: Metadata = {
@@ -34,6 +36,7 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
const perPage = 12;
const aiSearchAvailable = await isAISearchEnabled();
const aiGenerationAvailable = await isAIGenerationEnabled();
const useAISearch = aiSearchAvailable && params.ai === "1" && params.q;
let prompts: any[] = [];
@@ -177,7 +180,7 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
{!config.homepage?.useCloneBranding && (
<HFDataStudioDropdown />
<HFDataStudioDropdown aiGenerationEnabled={aiGenerationAvailable} />
)}
<Button size="sm" className="h-8 text-xs w-full sm:w-auto" asChild>
<Link href="/prompts/new">
@@ -188,29 +191,31 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {
</div>
</div>
<div className="flex flex-col lg:flex-row gap-6">
<aside className="w-full lg:w-56 shrink-0">
<PromptFilters
categories={categories}
tags={tags}
currentFilters={params}
aiSearchEnabled={aiSearchAvailable}
/>
</aside>
<main className="flex-1 min-w-0">
<InfinitePromptList
initialPrompts={prompts}
initialTotal={total}
filters={{
q: params.q,
type: params.type,
category: params.category,
tag: params.tag,
sort: params.sort,
}}
/>
</main>
</div>
<FilterProvider>
<div className="flex flex-col lg:flex-row gap-6">
<aside className="w-full lg:w-56 shrink-0">
<PromptFilters
categories={categories}
tags={tags}
currentFilters={params}
aiSearchEnabled={aiSearchAvailable}
/>
</aside>
<main className="flex-1 min-w-0">
<InfinitePromptList
initialPrompts={prompts}
initialTotal={total}
filters={{
q: params.q,
type: params.type,
category: params.category,
tag: params.tag,
sort: params.sort,
}}
/>
</main>
</div>
</FilterProvider>
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
"use client";
import { createContext, useContext, useState, ReactNode } from "react";
interface FilterContextType {
isFilterPending: boolean;
setFilterPending: (pending: boolean) => void;
}
const FilterContext = createContext<FilterContextType | null>(null);
export function FilterProvider({ children }: { children: ReactNode }) {
const [isFilterPending, setFilterPending] = useState(false);
return (
<FilterContext.Provider value={{ isFilterPending, setFilterPending }}>
{children}
</FilterContext.Provider>
);
}
export function useFilterContext() {
const context = useContext(FilterContext);
if (!context) {
throw new Error("useFilterContext must be used within FilterProvider");
}
return context;
}
@@ -3,9 +3,11 @@
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
import { ChevronDown, Play, ExternalLink } from "lucide-react";
import { ChevronDown, Play, ExternalLink, Sparkles, Loader2 } from "lucide-react";
import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Popover,
PopoverContent,
@@ -24,10 +26,16 @@ const DEFAULT_SQL = SQL_EXAMPLES[0].sql;
const HF_DATASET_URL = "https://huggingface.co/datasets/fka/awesome-chatgpt-prompts/viewer";
export function HFDataStudioDropdown() {
interface HFDataStudioDropdownProps {
aiGenerationEnabled?: boolean;
}
export function HFDataStudioDropdown({ aiGenerationEnabled = false }: HFDataStudioDropdownProps) {
const t = useTranslations("prompts.hfDataStudio");
const { resolvedTheme } = useTheme();
const [sql, setSql] = useState(DEFAULT_SQL);
const [aiPrompt, setAiPrompt] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const handleOpenDataset = () => {
window.open(HF_DATASET_URL, "_blank");
@@ -39,6 +47,28 @@ export function HFDataStudioDropdown() {
window.open(url, "_blank");
};
const handleGenerateSQL = async () => {
if (!aiPrompt.trim() || isGenerating) return;
setIsGenerating(true);
try {
const response = await fetch("/api/generate/sql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: aiPrompt }),
});
if (!response.ok) throw new Error("Failed to generate SQL");
const data = await response.json();
setSql(data.sql);
} catch (error) {
console.error("SQL generation error:", error);
} finally {
setIsGenerating(false);
}
};
return (
<div className="flex w-full sm:w-auto">
<Button
@@ -60,20 +90,19 @@ export function HFDataStudioDropdown() {
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[calc(100vw-2rem)] sm:w-[500px] p-0 max-sm:!fixed max-sm:!left-4 max-sm:!right-4 max-sm:!top-auto" sideOffset={8}>
<div className="p-3 space-y-3">
<div className="flex items-center justify-between gap-2">
<Select onValueChange={(value) => setSql(value)}>
<SelectTrigger className="w-[200px] h-7 text-xs">
<SelectValue placeholder={t("examples")} />
</SelectTrigger>
<SelectContent>
{SQL_EXAMPLES.map((example, index) => (
<SelectItem key={index} value={example.sql} className="text-xs">
{example.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Tabs defaultValue={aiGenerationEnabled ? "ai" : "examples"} className="w-full">
<div className="flex items-center justify-between px-3 pt-3 pb-2">
<TabsList className="h-7">
{aiGenerationEnabled && (
<TabsTrigger value="ai" className="text-xs h-6 px-2">
<Sparkles className="h-3 w-3 mr-1" />
{t("aiGenerate")}
</TabsTrigger>
)}
<TabsTrigger value="examples" className="text-xs h-6 px-2">
{t("examples")}
</TabsTrigger>
</TabsList>
<a
href={HF_DATASET_URL}
target="_blank"
@@ -83,29 +112,74 @@ export function HFDataStudioDropdown() {
{t("openDataset")} <ExternalLink className="h-3 w-3" />
</a>
</div>
<div className="border rounded-md overflow-hidden">
<Editor
height="200px"
defaultLanguage="sql"
value={sql}
onChange={(value) => setSql(value || "")}
theme={resolvedTheme === "dark" ? "vs-dark" : "light"}
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: "on",
}}
/>
{aiGenerationEnabled && (
<TabsContent value="ai" className="px-3 pb-3 mt-0 space-y-2">
<div className="flex gap-2">
<Input
placeholder={t("aiPlaceholder")}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleGenerateSQL()}
className="h-8 text-xs flex-1"
/>
<Button
size="sm"
variant="secondary"
className="h-8 px-3"
onClick={handleGenerateSQL}
disabled={isGenerating || !aiPrompt.trim()}
>
{isGenerating ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Sparkles className="h-3.5 w-3.5" />
)}
</Button>
</div>
</TabsContent>
)}
<TabsContent value="examples" className="px-3 pb-3 mt-0">
<Select onValueChange={(value) => setSql(value)}>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder={t("selectExample")} />
</SelectTrigger>
<SelectContent>
{SQL_EXAMPLES.map((example, index) => (
<SelectItem key={index} value={example.sql} className="text-xs">
{example.label}
</SelectItem>
))}
</SelectContent>
</Select>
</TabsContent>
<div className="px-3 pb-3 space-y-3">
<div className="border rounded-md overflow-hidden">
<Editor
height="200px"
defaultLanguage="sql"
value={sql}
onChange={(value) => setSql(value || "")}
theme={resolvedTheme === "dark" ? "vs-dark" : "light"}
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: "on",
}}
/>
</div>
<Button size="sm" className="w-full" onClick={handleRun}>
<Play className="h-3.5 w-3.5 mr-1.5" />
{t("runQuery")}
</Button>
</div>
<Button size="sm" className="w-full" onClick={handleRun}>
<Play className="h-3.5 w-3.5 mr-1.5" />
{t("runQuery")}
</Button>
</div>
</Tabs>
</PopoverContent>
</Popover>
</div>
@@ -1,8 +1,11 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { Loader2, SearchX } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { useFilterContext } from "./filter-context";
import { PromptCard, type PromptCardProps } from "./prompt-card";
interface InfinitePromptListProps {
@@ -17,24 +20,49 @@ interface InfinitePromptListProps {
};
}
function PromptCardSkeleton() {
return (
<div className="border rounded-lg p-4 space-y-3">
<div className="flex items-start justify-between">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-5 w-5 rounded" />
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-2/3" />
<div className="flex items-center gap-2 pt-2">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="h-4 w-24" />
</div>
<div className="flex gap-2">
<Skeleton className="h-5 w-14 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
</div>
);
}
export function InfinitePromptList({
initialPrompts,
initialTotal,
filters
}: InfinitePromptListProps) {
const t = useTranslations("prompts");
const searchParams = useSearchParams();
const { isFilterPending, setFilterPending } = useFilterContext();
const [prompts, setPrompts] = useState(initialPrompts);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(initialPrompts.length < initialTotal);
const loaderRef = useRef<HTMLDivElement>(null);
// Reset when filters change
// Reset when new data arrives from server
useEffect(() => {
setPrompts(initialPrompts);
setPage(1);
setHasMore(initialPrompts.length < initialTotal);
}, [initialPrompts, initialTotal]);
setFilterPending(false);
}, [initialPrompts, initialTotal, setFilterPending]);
const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
@@ -88,6 +116,17 @@ export function InfinitePromptList({
return () => observer.disconnect();
}, [loadMore, hasMore, isLoading]);
// Show skeleton while filtering
if (isFilterPending) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 12 }).map((_, i) => (
<PromptCardSkeleton key={i} />
))}
</div>
);
}
if (prompts.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
+36 -6
View File
@@ -1,7 +1,9 @@
"use client";
import { useState, useMemo, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useFilterContext } from "./filter-context";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
@@ -13,7 +15,7 @@ import {
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { X, Sparkles } from "lucide-react";
import { X, Sparkles, Search } from "lucide-react";
interface PromptFiltersProps {
categories: Array<{
@@ -45,6 +47,15 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations();
const [tagSearch, setTagSearch] = useState("");
const { setFilterPending } = useFilterContext();
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const filteredTags = useMemo(() => {
if (!tagSearch.trim()) return tags;
const search = tagSearch.toLowerCase();
return tags.filter((tag) => tag.name.toLowerCase().includes(search));
}, [tags, tagSearch]);
const updateFilter = (key: string, value: string | null) => {
const params = new URLSearchParams(searchParams.toString());
@@ -85,10 +96,17 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable
className="h-8 text-sm"
defaultValue={currentFilters.q}
onChange={(e) => {
const timeout = setTimeout(() => {
updateFilter("q", e.target.value || null);
const value = e.target.value;
// Show loading immediately
setFilterPending(true);
// Clear previous debounce
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// Debounce the actual navigation
debounceRef.current = setTimeout(() => {
updateFilter("q", value || null);
}, 300);
return () => clearTimeout(timeout);
}}
/>
</div>
@@ -185,8 +203,17 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable
{tags.length > 0 && (
<div className="space-y-1.5">
<Label className="text-xs">{t("prompts.promptTags")}</Label>
<div className="flex flex-wrap gap-1">
{tags.filter((t) => t.id && t.slug).slice(0, 8).map((tag) => (
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<Input
placeholder={t("search.searchTags")}
className="h-7 text-xs pl-7"
value={tagSearch}
onChange={(e) => setTagSearch(e.target.value)}
/>
</div>
<div className="flex flex-wrap gap-1 max-h-48 overflow-y-auto">
{filteredTags.filter((tag) => tag.id && tag.slug).map((tag) => (
<button
key={tag.id}
className="px-2 py-0.5 text-[11px] rounded border transition-colors"
@@ -200,6 +227,9 @@ export function PromptFilters({ categories, tags, currentFilters, aiSearchEnable
{tag.name}
</button>
))}
{filteredTags.length === 0 && tagSearch && (
<span className="text-xs text-muted-foreground">{t("search.noResults")}</span>
)}
</div>
</div>
)}
+19 -13
View File
@@ -11,13 +11,15 @@ function getOpenAIClient(): OpenAI {
if (!apiKey) {
throw new Error("OPENAI_API_KEY is not set");
}
openai = new OpenAI({ apiKey });
openai = new OpenAI({
apiKey,
baseURL: process.env.OPENAI_BASE_URL || undefined,
});
}
return openai;
}
const EMBEDDING_MODEL = "text-embedding-3-small";
const EMBEDDING_DIMENSIONS = 1536;
const EMBEDDING_MODEL = process.env.OPENAI_EMBEDDING_MODEL || "text-embedding-3-small";
export async function generateEmbedding(text: string): Promise<number[]> {
const client = getOpenAIClient();
@@ -193,16 +195,20 @@ export async function semanticSearch(
},
});
// Calculate similarity scores
const scoredPrompts = prompts.map((prompt) => {
const embedding = prompt.embedding as number[];
const similarity = cosineSimilarity(queryEmbedding, embedding);
return {
...prompt,
similarity,
voteCount: prompt._count.votes,
};
});
// Calculate similarity scores and filter by threshold
const SIMILARITY_THRESHOLD = 0.4; // Filter out results below this similarity
const scoredPrompts = prompts
.map((prompt) => {
const embedding = prompt.embedding as number[];
const similarity = cosineSimilarity(queryEmbedding, embedding);
return {
...prompt,
similarity,
voteCount: prompt._count.votes,
};
})
.filter((prompt) => prompt.similarity >= SIMILARITY_THRESHOLD);
// Sort by similarity and return top results
scoredPrompts.sort((a, b) => b.similarity - a.similarity);
+67
View File
@@ -0,0 +1,67 @@
import OpenAI from "openai";
import { getConfig } from "@/lib/config";
let openai: OpenAI | null = null;
function getOpenAIClient(): OpenAI {
if (!openai) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY is not set");
}
openai = new OpenAI({
apiKey,
baseURL: process.env.OPENAI_BASE_URL || undefined,
});
}
return openai;
}
const GENERATIVE_MODEL = process.env.OPENAI_GENERATIVE_MODEL || "gpt-4o-mini";
export async function isAIGenerationEnabled(): Promise<boolean> {
const config = await getConfig();
return !!(config.features.aiGeneration && process.env.OPENAI_API_KEY);
}
export async function generateSQL(prompt: string): Promise<string> {
const config = await getConfig();
if (!config.features.aiGeneration) {
throw new Error("AI Generation is not enabled");
}
const client = getOpenAIClient();
const systemPrompt = `You are an SQL expert. Generate SQL queries for the Hugging Face datasets viewer.
The dataset is "fka/awesome-chatgpt-prompts" with a "train" split.
Available columns: act (prompt title), prompt (prompt content), for_devs (boolean), type (TEXT or STRUCTURED), contributor (author name).
Rules:
- Always use "train" as the table name
- Return ONLY the SQL query, no explanations
- Keep queries simple and efficient
- Use proper SQL syntax for DuckDB
- The generated SQL strings must be in English since all prompts are in English (e.g. if user searches for "çiçekçi" it's florist.)
- Use multiple matchers like SELECT act, prompt FROM train WHERE LOWER(prompt) LIKE '%travel%' OR LOWER(act) LIKE '%travel%' OR LOWER(act) LIKE '%guide%' LIMIT 20;
- Be generative and creative. e.g. if user wrote "joke", search for similar terms.
- SQL must be formatted into lines.`;
const response = await client.chat.completions.create({
model: GENERATIVE_MODEL,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt }
],
temperature: 0.7,
max_tokens: 500,
});
const content = response.choices[0]?.message?.content || "";
// Clean up the response - remove markdown code blocks if present
return content
.replace(/^```sql\n?/i, "")
.replace(/^```\n?/i, "")
.replace(/\n?```$/i, "")
.trim();
}
+2
View File
@@ -44,6 +44,7 @@ export interface FeaturesConfig {
categories: boolean;
tags: boolean;
aiSearch?: boolean;
aiGeneration?: boolean;
}
export interface Sponsor {
@@ -125,6 +126,7 @@ export async function getConfig(): Promise<PromptsConfig> {
categories: true,
tags: true,
aiSearch: false,
aiGeneration: false,
},
};
return cachedConfig;