chore(messages): update AI features in multiple language files
This commit is contained in:
+4
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user