From 7c1e6ebd6df55c1a18877abc396a8479648e072f Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 09:56:52 +0200 Subject: [PATCH 01/30] Final sync --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6427b2a..8204068 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? +.agent # --- Dependencies --- node_modules/ From dd87efb960afe0390e2d5f4c7282cd03d4b5672d Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 11:41:43 +0200 Subject: [PATCH 02/30] build: prepare release v1.1.1 (smart tags, state sync fixes) --- CHANGELOG.md | 13 +++++++++++ README.md | 3 ++- package.json | 2 +- src/App.tsx | 13 +++++++++-- src/components/DataActions.tsx | 41 ++++++++++++++++++++++++---------- src/hooks/useLocalStorage.ts | 28 ++++++++++++++++++++++- 6 files changed, 83 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02951e9..4b67882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2026-03-27 + +### Added +- **Tag Autocomplete**: Built-in form autocomplete when typing tags based on your existing library. + +### Changed +- Relocated the **Grid/List View Toggle** next to the search bar for an improved user experience. + +### Fixed +- **State Sync Bug**: Fixed a core rendering issue where JSON Import and Cloud Sync operations wouldn't automatically re-render the prompt list. +- Removed intrusive browser `window.confirm` pop-ups from automated sync flows to prevent PWA blocking limits. + ## [1.1.0] - 2026-03-27 ### Added @@ -36,5 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MIT License and basic README documentation. - Automated deployment to GitHub Pages via `gh-pages`. +[1.1.1]: https://github.com/NaviAndrei/PromptLibrary/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/NaviAndrei/PromptLibrary/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/NaviAndrei/PromptLibrary/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 8867769..f7b9e7c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A sleek, production-quality web application designed to help AI enthusiasts and - **☁️ Cloud Sync**: Securely sync your prompts using GitHub Gists (No database needed!). - **📑 Rich Formatting**: Markdown support with syntax highlighting for code blocks. - **🔍 Advanced Search**: Instant filtering by title, body, or tags. -- **🏷️ Tags Cloud**: Interactive sidebar to organize and filter your library. +- **🏷️ Smart Tags**: Interactive sidebar to organize your library and intelligent autocomplete when adding new prompts. - **💾 Data Control**: Export/Import your entire library as JSON or individual prompts as Markdown. - **📊 Token Estimator**: Real-time token count estimation for each prompt. - **🛡️ Secure & Private**: All data stays in your browser or your private GitHub Gist. @@ -68,6 +68,7 @@ npm run build - [x] JSON Import/Export & Markdown Export - [x] GitHub Gist Cloud Sync - [x] Markdown Rendering & Syntax Highlighting +- [x] Smart Tag Autocomplete - [ ] Dark Mode Toggle - [ ] Multiple Gist support for shared libraries - [ ] Advanced prompt templates with input variables diff --git a/package.json b/package.json index eaf4a00..01dfefd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "prompt-library", "private": false, "description": "A modern, sleek web application for managing AI prompts. Built with React, TypeScript, and Vite.", - "version": "1.1.0", + "version": "1.1.1", "author": "Ivan Andrei (NaviAndrei)", "license": "MIT", "type": "module", diff --git a/src/App.tsx b/src/App.tsx index 4eb3a32..8910480 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { LayoutGrid, List } from 'lucide-react'; function App() { // Stocăm array-ul de prompt-uri în localStorage const [prompts, setPrompts] = useLocalStorage('prompts', []); + console.log('App re-rendered with prompts:', prompts.length); // Stocăm query-ul de căutare din SearchBar const [searchQuery, setSearchQuery] = useState(''); @@ -75,12 +76,20 @@ function App() { createdAt: now, updatedAt: now }; - // Adăugăm prima dată noul prompt + // 2. Adăugăm prima dată noul prompt setPrompts([newPrompt, ...prompts]); toast.success('Prompt creat cu succes!'); } }; + // Funcția pentru a procesa importul JSON + const handleImportPrompts = (imported: Prompt[]) => { + setPrompts(imported); + // Salvăm și direct în localStorage pentru siguranță maximă imediată + window.localStorage.setItem('prompts', JSON.stringify(imported)); + toast.success(`Am importat ${imported.length} prompt-uri!`); + }; + // Funcția pentru a deschide formularul pre-completat pentru un prompt existent const handleEditPrompt = (prompt: Prompt) => { setEditingPrompt(prompt); @@ -111,7 +120,7 @@ function App() {

Prompt Library

Gestionează, filtrează și sincronizează prompt-urile AI direct din browser.

- +
diff --git a/src/components/DataActions.tsx b/src/components/DataActions.tsx index 02db3a3..061ceb2 100644 --- a/src/components/DataActions.tsx +++ b/src/components/DataActions.tsx @@ -38,19 +38,28 @@ export function DataActions({ prompts, onImport }: DataActionsProps) { const reader = new FileReader(); reader.onload = (event) => { + console.log('FileReader onload triggered'); try { - const parsed = JSON.parse(event.target?.result as string); + const result = event.target?.result as string; + console.log('File read result length:', result?.length); + const parsed = JSON.parse(result); + console.log('JSON parsed, isArray:', Array.isArray(parsed)); + if (Array.isArray(parsed) && parsed.every(p => p.title && p.body && p.id)) { - // Dacă vrem să suprascriem sau să facem append, acum facem simplu overwrite: - if (window.confirm(`Gata de a suprascrie baza de date cu ${parsed.length} prompt-uri?`)) { - onImport(parsed); - toast.success(`${parsed.length} prompt-uri importate con succes!`); - } + console.log('JSON structure is valid. Attempting import...', parsed.length); + // Importăm direct + onImport(parsed); + // Dispecerizează un eveniment custom pentru ca hook-ul să detecteze schimbarea + window.dispatchEvent(new Event('storage-sync')); + console.log('Dispatched storage-sync event.'); + toast.success(`${parsed.length} prompt-uri importate cu succes!`); } else { + console.warn('Import eșuat: Structura JSON invalidă.', parsed); toast.error('Fișierul JSON nu are formatul corect de Prompt[].'); } - } catch { - toast.error('Eroare la citirea fișierului JSON.'); + } catch (err) { + console.error('Eroare parsing JSON:', err); + toast.error('Eroare la citirea sau procesarea fișierului JSON.'); } if (fileInputRef.current) fileInputRef.current.value = ''; }; @@ -102,10 +111,12 @@ export function DataActions({ prompts, onImport }: DataActionsProps) { }; const syncFromGist = async () => { + console.log('syncFromGist started. Token:', !!githubToken, 'GistID:', gistId); if (!githubToken || !gistId) return toast.error('Token și Gist ID sunt necesare pentru a citi din cloud!'); setIsSyncing(true); try { + console.log('Fetching gist from GitHub API...'); const res = await fetch(`https://api.github.com/gists/${gistId}`, { headers: { 'Authorization': `token ${githubToken}`, @@ -113,18 +124,24 @@ export function DataActions({ prompts, onImport }: DataActionsProps) { } }); + console.log('Fetch response status:', res.status); if (!res.ok) throw new Error('Nu am găsit Gist-ul'); const data = await res.json(); + console.log('Gist data received. Files:', Object.keys(data.files)); const fileData = data.files['prompt-library-db.json']; if (!fileData) throw new Error('Gist-ul nu conține fișierul așteptat'); const parsed = JSON.parse(fileData.content); + console.log('Gist JSON parsed successfully:', parsed.length, 'items'); if (Array.isArray(parsed)) { - if (window.confirm(`Acest cont conține ${parsed.length} prompt-uri. Suprascrii local?`)) { - onImport(parsed); - toast.success(`Au fost încărcate ${parsed.length} prompt-uri din Cloud!`); - } + console.log('Gist data is an array. Calling onImport...'); + onImport(parsed); + window.dispatchEvent(new Event('storage-sync')); + console.log('Dispatched storage-sync event for Gist.'); + toast.success(`Au fost încărcate ${parsed.length} prompt-uri din Cloud!`); + } else { + console.warn('Gist data is not an array:', parsed); } } catch (error) { console.error(error); diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index c9c9947..2376a64 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; // Hook generic pentru a stoca și citi date din localStorage // Utilizează inițializare "leneșă" (lazy initialization) pentru ca parsarea JSON să se facă doar la prima randare @@ -16,6 +16,32 @@ export function useLocalStorage(key: string, initialValue: T) { } }); + // Ascultăm schimbări externe în localStorage (ex: importuri din DataActions) + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key || e.key === null) { + try { + const item = window.localStorage.getItem(key); + const newValue = item ? JSON.parse(item) : initialValue; + setStoredValue(newValue); + console.log(`Storage sync triggered for key: ${key}`, newValue?.length); + } catch (err) { + console.error('Eroare la sincronizarea automată:', err); + } + } + }; + + window.addEventListener('storage', handleStorageChange); + // De asemenea ascultăm evenimente custom trimise manual cu dispatchEvent(new Event('storage')) + const handleCustomStorage = () => handleStorageChange({ key } as StorageEvent); + window.addEventListener('storage-sync', handleCustomStorage); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('storage-sync', handleCustomStorage); + }; + }, [key, initialValue]); + // Această funcție o prinde pe cea standard din React pentru a salva în localStorage simultan const setValue = (value: T | ((val: T) => T)) => { try { From bc952dbfdd16c225f929fa77fa0fb7f8d6131ee3 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 11:52:10 +0200 Subject: [PATCH 03/30] style: fix grid masonry view layout logic --- src/index.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index 213ebdb..7edf0b7 100644 --- a/src/index.css +++ b/src/index.css @@ -331,12 +331,15 @@ button { /* Lista și Elementele Prompt */ .prompt-list.grid { - columns: 1 20rem; /* Native CSS Masonry */ - gap: 1.5rem; + column-width: 18rem; /* Vor încăpea natural 2 coloane lângă sidebar */ + column-gap: 1.5rem; } .prompt-list.grid > .prompt-card { break-inside: avoid-column; + page-break-inside: avoid; + display: inline-block; + width: 100%; margin-bottom: 1.5rem; } From 84d8fb86c3113348f94ebf95f392557248c03820 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 11:55:06 +0200 Subject: [PATCH 04/30] style: increase container width for masonry grid overflow --- src/index.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.css b/src/index.css index 7edf0b7..3460d2a 100644 --- a/src/index.css +++ b/src/index.css @@ -34,9 +34,9 @@ padding: 0; } -/* Container principal ajustat pe centru cu 900px lățime max */ +/* Container principal ajustat pe centru cu 1100px lățime max pentru a permite view de grid generos */ .container { - max-width: 900px; + max-width: 1100px; margin: 0 auto; padding: 2rem 1rem; } @@ -331,7 +331,7 @@ button { /* Lista și Elementele Prompt */ .prompt-list.grid { - column-width: 18rem; /* Vor încăpea natural 2 coloane lângă sidebar */ + column-width: 16rem; /* Scalare corectă pentru minim 2 coloane lângă sidebar */ column-gap: 1.5rem; } From d50292b1f4ccc8c378673ee3732522f720d81dc7 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 11:56:59 +0200 Subject: [PATCH 05/30] style: expand container max-width to 1400px for 3+ column layouts --- src/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index 3460d2a..3e6c845 100644 --- a/src/index.css +++ b/src/index.css @@ -34,9 +34,9 @@ padding: 0; } -/* Container principal ajustat pe centru cu 1100px lățime max pentru a permite view de grid generos */ +/* Container principal ajustat pe centru cu 1400px lățime max pentru a permite view de grid generos (3+ coloane) pe monitoare mari */ .container { - max-width: 1100px; + max-width: 1400px; margin: 0 auto; padding: 2rem 1rem; } From f5d6f79b9b86d0c3cde6ed52a73bae3881419713 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 11:58:18 +0200 Subject: [PATCH 06/30] style: force 3 columns natively using column-count and media queries --- src/index.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index 3e6c845..a4663c5 100644 --- a/src/index.css +++ b/src/index.css @@ -331,10 +331,22 @@ button { /* Lista și Elementele Prompt */ .prompt-list.grid { - column-width: 16rem; /* Scalare corectă pentru minim 2 coloane lângă sidebar */ + column-count: 3; /* Forțăm browser-ul să folosească 3 coloane pe rezoluții mari */ column-gap: 1.5rem; } +@media (max-width: 1200px) { + .prompt-list.grid { + column-count: 2; + } +} + +@media (max-width: 768px) { + .prompt-list.grid { + column-count: 1; + } +} + .prompt-list.grid > .prompt-card { break-inside: avoid-column; page-break-inside: avoid; From 290f8ae8e21a9ef7327e90a9056823fc6ed015bd Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 12:02:59 +0200 Subject: [PATCH 07/30] feat: add global dark mode toggle --- src/App.tsx | 23 +++++++++++++++++---- src/index.css | 55 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8910480..f642110 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import type { Prompt } from './types'; import { useLocalStorage } from './hooks/useLocalStorage'; import { SearchBar } from './components/SearchBar'; @@ -7,7 +7,7 @@ import { PromptList } from './components/PromptList'; import { Sidebar } from './components/Sidebar'; import { DataActions } from './components/DataActions'; import { Toaster, toast } from 'sonner'; -import { LayoutGrid, List } from 'lucide-react'; +import { LayoutGrid, List, Moon, Sun } from 'lucide-react'; function App() { // Stocăm array-ul de prompt-uri în localStorage @@ -23,6 +23,12 @@ function App() { // UI state const [viewMode, setViewMode] = useState<'list' | 'grid'>('grid'); const [selectedTag, setSelectedTag] = useState(null); + const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); + + // Aplică tema globală la nivel de HTML tag + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); // Compute all unique tags for the sidebar const allTags = useMemo(() => { @@ -113,14 +119,23 @@ function App() { return (
- +

Prompt Library

Gestionează, filtrează și sincronizează prompt-urile AI direct din browser.

- +
+ + +
diff --git a/src/index.css b/src/index.css index a4663c5..281c430 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,6 @@ /* CSS Modern și Minimal pentru Prompt Library */ -:root { +:root, [data-theme="light"] { /* Paleta de culori Light-Premium */ --bg-color: #f8fafc; --surface-color: #ffffff; @@ -16,17 +16,37 @@ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --radius-md: 0.5rem; --radius-lg: 0.75rem; +} + +[data-theme="dark"] { + /* Paleta de culori Dark-Premium */ + --bg-color: #0f172a; + --surface-color: #1e293b; + --primary-color: #3b82f6; + --primary-hover: #60a5fa; + --text-dark: #f8fafc; + --text-muted: #94a3b8; + --border-color: #334155; + --danger-color: #ef4444; + --danger-hover: #f87171; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4); +} + +body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; color: var(--text-dark); background-color: var(--bg-color); + transition: background-color 0.3s ease, color 0.3s ease; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + /* Reset de bază */ * { box-sizing: border-box; @@ -189,12 +209,13 @@ button { } .btn-secondary { - background-color: #f1f5f9; + background-color: var(--bg-color); color: var(--text-dark); + border: 1px solid var(--border-color); } .btn-secondary:hover { - background-color: #e2e8f0; + background-color: var(--border-color); } .btn-icon { @@ -204,13 +225,13 @@ button { } .btn-icon:hover { - background-color: #f1f5f9; + background-color: var(--bg-color); color: var(--text-dark); } .btn-icon.delete:hover { color: var(--danger-color); - background-color: #fef2f2; + background-color: var(--bg-color); } /* Statisticile listei */ @@ -236,7 +257,7 @@ button { } .view-toggle .btn-icon.active { - background: #e2e8f0; + background: var(--border-color); color: var(--primary-color); } @@ -296,7 +317,7 @@ button { } .tags-list li:hover { - background: #f1f5f9; + background: var(--bg-color); color: var(--text-dark); } @@ -313,7 +334,7 @@ button { .tag-name { font-weight: 500; } .tag-count { font-size: 0.75rem; - background: #e2e8f0; + background: var(--border-color); padding: 2px 6px; border-radius: 99px; color: var(--text-muted); @@ -382,8 +403,8 @@ button { .model-badge { display: inline-block; font-size: 0.75rem; - background-color: #e0e7ff; - color: #4338ca; + background-color: var(--border-color); + color: var(--primary-color); padding: 0.2rem 0.5rem; border-radius: 999px; font-weight: 600; @@ -399,7 +420,7 @@ button { .tag-badge { font-size: 0.8rem; - background-color: #f1f5f9; + background-color: var(--bg-color); color: var(--text-dark); padding: 0.2rem 0.6rem; border-radius: 4px; @@ -407,8 +428,8 @@ button { .token-badge { font-size: 0.75rem; - background-color: #fef08a; - color: #854d0e; + background-color: var(--danger-color); + color: white; padding: 0.2rem 0.6rem; border-radius: 4px; font-weight: 500; @@ -417,7 +438,7 @@ button { /* Zona expandabilă de conținut textual (body) */ .prompt-body-preview { margin-top: 1rem; - background-color: #f8fafc; + background-color: var(--bg-color); padding: 1rem; border-radius: var(--radius-md); border: 1px solid var(--border-color); @@ -506,14 +527,14 @@ button { width: 100%; margin-top: 1rem; padding: 0.5rem; - background: #f1f5f9; + background: var(--bg-color); color: var(--text-muted); border-radius: var(--radius-md); transition: all 0.2s; } .expand-btn:hover { - background: #e2e8f0; + background: var(--border-color); color: var(--text-dark); } @@ -544,7 +565,7 @@ button { margin-bottom: 1rem; } .markdown-body code { - background: #f1f5f9; + background: var(--border-color); padding: 0.2rem 0.4rem; border-radius: 4px; font-family: 'Consolas', monospace; From 223af2a11f12eea9d622094e57dac7335e09272c Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 27 Mar 2026 21:35:08 +0200 Subject: [PATCH 08/30] feat: improve multi-tag autocomplete in PromptForm --- src/components/PromptForm.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/PromptForm.tsx b/src/components/PromptForm.tsx index a52643a..4a5d9a3 100644 --- a/src/components/PromptForm.tsx +++ b/src/components/PromptForm.tsx @@ -76,11 +76,27 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro onChange={e => setTagsInput(e.target.value)} placeholder="Ex: refactor, python, clean-code" list="tags-autocomplete" + autoComplete="off" /> - {existingTags.map(tag => ( -
From 0d4550c498e04e5d5fa79de9d8f13903b77955da Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Sat, 28 Mar 2026 01:05:56 +0200 Subject: [PATCH 09/30] feat: Smart Workspaces, Variable Injector, Version History with LCS diff --- src/App.tsx | 148 ++++++++---- src/components/PromptForm.tsx | 72 ++++-- src/components/PromptList.tsx | 234 +++++++++++-------- src/components/Sidebar.tsx | 54 +++-- src/components/VariableInjector.tsx | 75 +++++++ src/components/VersionHistory.tsx | 135 +++++++++++ src/components/WorkspaceManager.tsx | 141 ++++++++++++ src/index.css | 336 ++++++++++++++++++++++++++++ src/types.ts | 32 ++- 9 files changed, 1036 insertions(+), 191 deletions(-) create mode 100644 src/components/VariableInjector.tsx create mode 100644 src/components/VersionHistory.tsx create mode 100644 src/components/WorkspaceManager.tsx diff --git a/src/App.tsx b/src/App.tsx index f642110..46bb4ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,31 @@ import { useState, useMemo, useEffect } from 'react'; -import type { Prompt } from './types'; +import type { Prompt, Workspace, PromptVersion } from './types'; import { useLocalStorage } from './hooks/useLocalStorage'; import { SearchBar } from './components/SearchBar'; import { PromptForm } from './components/PromptForm'; import { PromptList } from './components/PromptList'; import { Sidebar } from './components/Sidebar'; import { DataActions } from './components/DataActions'; +import { WorkspaceManager } from './components/WorkspaceManager'; import { Toaster, toast } from 'sonner'; import { LayoutGrid, List, Moon, Sun } from 'lucide-react'; function App() { // Stocăm array-ul de prompt-uri în localStorage const [prompts, setPrompts] = useLocalStorage('prompts', []); - console.log('App re-rendered with prompts:', prompts.length); - + + // Stocăm workspace-urile în localStorage + const [workspaces, setWorkspaces] = useLocalStorage('workspaces', []); + + // Stocăm istoricul versiunilor în localStorage (indexat după promptId) + const [promptHistory, setPromptHistory] = useLocalStorage('prompt-history', []); + + // Workspace-ul selectat curent (null = toate prompt-urile) + const [currentWorkspaceId, setCurrentWorkspaceId] = useState(null); + // Stocăm query-ul de căutare din SearchBar const [searchQuery, setSearchQuery] = useState(''); - + // Stocăm prompt-ul pe care îl edităm în mod curent const [editingPrompt, setEditingPrompt] = useState(null); @@ -39,14 +48,21 @@ function App() { return Object.entries(counts).sort((a, b) => b[1] - a[1]); }, [prompts]); - // Căutare și filtrare + // Căutare și filtrare (inclusiv după workspace) const filteredPrompts = useMemo(() => { let result = prompts; - + + // Filtrare după workspace selectat + if (currentWorkspaceId) { + result = result.filter(p => p.workspaceId === currentWorkspaceId); + } + + // Filtrare după tag selectat if (selectedTag) { result = result.filter(p => p.tags.includes(selectedTag)); } - + + // Filtrare după textul de căutare if (searchQuery.trim()) { const lowerQuery = searchQuery.toLowerCase(); result = result.filter(p => { @@ -56,19 +72,27 @@ function App() { return matchesTitle || matchesBody || matchesTags; }); } - + return result; - }, [prompts, searchQuery, selectedTag]); + }, [prompts, searchQuery, selectedTag, currentWorkspaceId]); // Handler pentru Creare sau Update complet const handleSavePrompt = (promptData: Omit) => { const now = new Date().toISOString(); - + if (editingPrompt) { - // Dacă doar se modifică, facem map peste tot array-ul și înlocuim valoarea unde ID-urile coincid - const updatedPrompts = prompts.map(p => - p.id === editingPrompt.id - ? { ...p, ...promptData, updatedAt: now } + // Capturăm un snapshot al versiunii curente ÎNAINTE de actualizare + const snapshot: PromptVersion = { + promptId: editingPrompt.id, + body: editingPrompt.body, + savedAt: now, + }; + setPromptHistory(prev => [snapshot, ...prev]); + + // Actualizăm prompt-ul + const updatedPrompts = prompts.map(p => + p.id === editingPrompt.id + ? { ...p, ...promptData, updatedAt: now } : p ); setPrompts(updatedPrompts); @@ -80,9 +104,9 @@ function App() { ...promptData, id: crypto.randomUUID(), createdAt: now, - updatedAt: now + updatedAt: now, + workspaceId: currentWorkspaceId ?? undefined, }; - // 2. Adăugăm prima dată noul prompt setPrompts([newPrompt, ...prompts]); toast.success('Prompt creat cu succes!'); } @@ -91,44 +115,59 @@ function App() { // Funcția pentru a procesa importul JSON const handleImportPrompts = (imported: Prompt[]) => { setPrompts(imported); - // Salvăm și direct în localStorage pentru siguranță maximă imediată window.localStorage.setItem('prompts', JSON.stringify(imported)); toast.success(`Am importat ${imported.length} prompt-uri!`); }; - // Funcția pentru a deschide formularul pre-completat pentru un prompt existent const handleEditPrompt = (prompt: Prompt) => { setEditingPrompt(prompt); - // Putem să dăm "scroll to top" ca re-asigurare că utilizatorul vede formularul window.scrollTo({ top: 0, behavior: 'smooth' }); }; - // Funcția de anulare a selecției const handleClearForm = () => { setEditingPrompt(null); }; - // Filtrează array-ul și îl exclude pe cel șters const handleDeletePrompt = (id: string) => { setPrompts(prompts.filter(p => p.id !== id)); + // Ștergem și istoricul versiunilor pentru acest prompt + setPromptHistory(prev => prev.filter(v => v.promptId !== id)); if (editingPrompt?.id === id) { setEditingPrompt(null); } toast.info('Promptul a fost șters.'); }; + // Handlers pentru workspace-uri + const handleAddWorkspace = (ws: Workspace) => { + setWorkspaces(prev => [...prev, ws]); + toast.success(`Workspace "${ws.name}" creat!`); + }; + + const handleDeleteWorkspace = (id: string) => { + setWorkspaces(prev => prev.filter(ws => ws.id !== id)); + // Dacă workspace-ul șters era selectat, revenim la "Toate" + if (currentWorkspaceId === id) setCurrentWorkspaceId(null); + toast.info('Workspace șters.'); + }; + + // Returnează versiunile pentru un anumit prompt + const getVersionsForPrompt = (promptId: string): PromptVersion[] => { + return promptHistory.filter(v => v.promptId === promptId); + }; + return (
- +

Prompt Library

Gestionează, filtrează și sincronizează prompt-urile AI direct din browser.

-
- - + +
-
- -
- t[0])} - onClear={handleClearForm} + onClear={handleClearForm} + workspaces={workspaces} + currentWorkspaceId={currentWorkspaceId} />
Total: {prompts.length} | Găsite: {filteredPrompts.length} + {currentWorkspaceId && workspaces.find(w => w.id === currentWorkspaceId) && ( + w.id === currentWorkspaceId)?.color, + color: 'white', + padding: '0.1rem 0.5rem', + borderRadius: '99px', + fontSize: '0.8rem', + }}> + {workspaces.find(w => w.id === currentWorkspaceId)?.icon} {workspaces.find(w => w.id === currentWorkspaceId)?.name} + + )}
-
diff --git a/src/components/PromptForm.tsx b/src/components/PromptForm.tsx index 4a5d9a3..b845046 100644 --- a/src/components/PromptForm.tsx +++ b/src/components/PromptForm.tsx @@ -1,31 +1,38 @@ import { useState } from 'react'; -import type { Prompt } from '../types'; +import type { Prompt, Workspace } from '../types'; interface PromptFormProps { onSave: (prompt: Omit) => void; editingPrompt: Prompt | null; existingTags: string[]; onClear: () => void; + workspaces: Workspace[]; + currentWorkspaceId: string | null; } -// Formular pentru crearea sau editarea unui prompt -export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: PromptFormProps) { +// Formarul pentru crearea sau editarea unui prompt +export function PromptForm({ onSave, editingPrompt, existingTags, onClear, workspaces, currentWorkspaceId }: PromptFormProps) { // Inițializăm state-ul din prima cu valorile dorite (se va re-rula datorită prop-ului 'key' din App.tsx) const [title, setTitle] = useState(editingPrompt?.title || ''); const [body, setBody] = useState(editingPrompt?.body || ''); const [tagsInput, setTagsInput] = useState(editingPrompt ? editingPrompt.tags.join(', ') : ''); const [model, setModel] = useState(editingPrompt?.model || 'GPT-4o'); + // Workspace-ul selectat în formular (implicit cel curent sau cel de pe prompt în editare) + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( + editingPrompt?.workspaceId ?? currentWorkspaceId ?? '' + ); const resetForm = () => { setTitle(''); setBody(''); setTagsInput(''); setModel('GPT-4o'); + setSelectedWorkspaceId(currentWorkspaceId ?? ''); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - + // Validări de bază (trim elimină spațiile goale exterioare) if (!title.trim() || !body.trim()) { alert('Titlul și corpul textului sunt necesare.'); @@ -33,7 +40,6 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro } // Parsăm tag-urile dintr-un string separat prin virgulă într-un array - // Eliminăm tag-urile goale const tagsArray = tagsInput .split(',') .map(t => t.trim()) @@ -45,6 +51,7 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro body: body.trim(), tags: tagsArray, model: model.trim() || 'Generic', + workspaceId: selectedWorkspaceId || undefined, }); // Curățăm formularul nativ doar dacă este o creare (nu o editare) @@ -56,13 +63,13 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro return (

{editingPrompt ? 'Editează Prompt' : 'Adaugă Prompt Nou'}

- +
- setTitle(e.target.value)} + setTitle(e.target.value)} placeholder="Ex: Refactorizare cod Python" />
@@ -70,9 +77,9 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro
- setTagsInput(e.target.value)} placeholder="Ex: refactor, python, clean-code" list="tags-autocomplete" @@ -83,12 +90,12 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro // Logica pentru autocomplete multi-tag folosind datalist nativ const parts = tagsInput.split(','); const lastPart = parts[parts.length - 1].trim(); - const prefix = parts.length > 1 - ? parts.slice(0, -1).join(', ') + ', ' + const prefix = parts.length > 1 + ? parts.slice(0, -1).join(', ') + ', ' : ''; return existingTags - .filter(tag => + .filter(tag => tag.toLowerCase().includes(lastPart.toLowerCase()) && !parts.map(p => p.trim().toLowerCase()).includes(tag.toLowerCase()) ) @@ -101,22 +108,41 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear }: Pro
- setModel(e.target.value)} placeholder="Ex: Claude 3.5 Sonnet" />
+ {/* Selector Workspace – apare doar dacă există workspace-uri create */} + {workspaces.length > 0 && ( +
+ + +
+ )} +
-