mnoorchenar commited on
Commit
578f8ce
·
1 Parent(s): db26343

Local changes before HF merge

Browse files
Files changed (7) hide show
  1. .gitignore +58 -3
  2. Dockerfile +0 -10
  3. MIGRATION_GUIDE.md +332 -0
  4. README.md +294 -57
  5. app.py +815 -7
  6. requirements.txt +2 -1
  7. sync.ps1 +18 -21
.gitignore CHANGED
@@ -1,5 +1,60 @@
 
1
  __pycache__/
2
- *.pyc
3
- .env
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  .DS_Store
5
- node_modules/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
  __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ build/
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Database files (keep in git for deployment, but backup locally)
27
+ # Uncomment next line if you don't want to track database in git
28
+ # *.db
29
+
30
+ # Backup files
31
+ *.backup
32
+ *.bak
33
+ *.db.backup
34
+
35
+ # IDE
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+ *~
41
+
42
+ # OS
43
  .DS_Store
44
+ Thumbs.db
45
+ desktop.ini
46
+
47
+ # Environment variables
48
+ .env
49
+ .env.local
50
+
51
+ # Gradio
52
+ flagged/
53
+ gradio_cached_examples/
54
+
55
+ # Logs
56
+ *.log
57
+
58
+ # Temporary files
59
+ tmp/
60
+ temp/
Dockerfile DELETED
@@ -1,10 +0,0 @@
1
- FROM python:3.9
2
-
3
- WORKDIR /app
4
-
5
- COPY requirements.txt .
6
- RUN pip install --no-cache-dir -r requirements.txt
7
-
8
- COPY . .
9
-
10
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
MIGRATION_GUIDE.md ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TextArchive - Migration Guide & New Features
2
+
3
+ ## 🎉 What Changed from "Prompt Manager" to "TextArchive"
4
+
5
+ ### Name Change
6
+ - **Old**: Prompt Manager
7
+ - **New**: TextArchive
8
+ - **Why**: More universal - supports all text types, not just prompts
9
+
10
+ ### Database Changes
11
+ - **Old**: `prompts.db` with table `prompts`
12
+ - **New**: `textarchive.db` with table `text_items`
13
+ - **Migration**: Automatic! Your old data is preserved and migrated
14
+
15
+ ---
16
+
17
+ ## ✨ NEW FEATURES IMPLEMENTED
18
+
19
+ ### 1. ✅ Easy Content Copying
20
+ **The Problem You Had:**
21
+ - Hard to copy just the text content
22
+ - Would copy entire row with ID, category, section
23
+
24
+ **The Solution:**
25
+ - Click any row in the table
26
+ - Content appears in "Content Preview" box below table
27
+ - Click the copy button (📋) OR select text and Ctrl+C
28
+ - Only the text content is copied - no metadata!
29
+
30
+ **How to Use:**
31
+ 1. Go to "View & Manage Items" tab
32
+ 2. Click on any row
33
+ 3. Look at "Content Preview" section below
34
+ 4. Click copy button or manually select and copy
35
+
36
+ ---
37
+
38
+ ### 2. ✅ Auto-Filtering (AJAX-Style)
39
+ **The Problem You Had:**
40
+ - Had to click "Search" button every time
41
+ - Not responsive
42
+
43
+ **The Solution:**
44
+ - Filters update automatically when you change them
45
+ - No search button needed!
46
+ - Instant feedback
47
+
48
+ **How to Use:**
49
+ 1. Just select a category → table updates immediately
50
+ 2. Select a section → table updates immediately
51
+ 3. Select a content type → table updates immediately
52
+ 4. Click "Clear Filters" to reset
53
+
54
+ ---
55
+
56
+ ### 3. ✅ Fixed Height Table with Scrolling
57
+ **The Problem You Had:**
58
+ - Table grew huge with many items
59
+ - Hard to navigate
60
+
61
+ **The Solution:**
62
+ - Table has fixed height (400px)
63
+ - Scrolls internally when content exceeds height
64
+ - Maintains clean, consistent interface
65
+
66
+ **Result:**
67
+ - Interface stays organized
68
+ - Easy to browse large collections
69
+ - Better user experience
70
+
71
+ ---
72
+
73
+ ### 4. ✅ Section Dropdown Filtering
74
+ **The Problem You Had:**
75
+ - Section dropdown showed ALL sections
76
+ - Confusing when you had many categories
77
+
78
+ **The Solution:**
79
+ - Section dropdown automatically filters by selected category
80
+ - Only shows sections that exist in that category
81
+ - Much cleaner and more intuitive
82
+
83
+ **How It Works:**
84
+ 1. Select a category
85
+ 2. Section dropdown automatically updates to show only relevant sections
86
+ 3. Works in both "Add" and "Edit" modes
87
+
88
+ ---
89
+
90
+ ### 5. ✅ Category & Section Renaming
91
+ **Critical Feature You Were Missing:**
92
+ - Could NOT rename categories/sections
93
+ - Changing name created a new one, leaving old data orphaned
94
+
95
+ **The Solution:**
96
+ - New "Manage Categories & Sections" tab
97
+ - Rename category → updates ALL items with that category
98
+ - Rename section → updates ALL items with that section
99
+ - Can rename section globally or within a specific category
100
+
101
+ **How to Use:**
102
+ 1. Go to "⚙️ Manage Categories & Sections" tab
103
+ 2. Select category or section to rename
104
+ 3. Enter new name
105
+ 4. Click rename button
106
+ 5. All items are updated automatically!
107
+
108
+ ---
109
+
110
+ ### 6. ✅ Bulk Deletion
111
+ **Critical Feature You Were Missing:**
112
+ - Could only delete one item at a time
113
+ - No way to clean up entire categories/sections
114
+
115
+ **The Solution:**
116
+ - Delete entire category (all items in it)
117
+ - Delete entire section (all items in it, optionally within a category)
118
+ - Confirmation required to prevent accidents
119
+ - Shows count of items that will be deleted
120
+
121
+ **How to Use:**
122
+ 1. Go to "⚙️ Manage Categories & Sections" tab
123
+ 2. Select category or section to delete
124
+ 3. Check the confirmation box
125
+ 4. Click delete button
126
+ 5. Status shows how many items were deleted
127
+
128
+ ---
129
+
130
+ ### 7. ✅ Content Type System
131
+ **New Capability:**
132
+ - Support for 11 different content types
133
+ - Each type has icon and file extension
134
+ - Better organization
135
+
136
+ **Supported Types:**
137
+ - 💬 Prompt (.txt)
138
+ - 📝 Note (.md)
139
+ - 🐍 Python (.py)
140
+ - 📜 JavaScript (.js)
141
+ - 📐 LaTeX (.tex)
142
+ - 🗄️ SQL (.sql)
143
+ - 💻 Bash (.sh)
144
+ - ⚙️ Function (.txt)
145
+ - 🌐 HTML (.html)
146
+ - 🎨 CSS (.css)
147
+ - 📄 Other (.txt)
148
+
149
+ **How to Use:**
150
+ 1. When adding or editing, select content type
151
+ 2. Type appears with icon in table view
152
+ 3. File extension is automatically set
153
+
154
+ ---
155
+
156
+ ### 8. ✅ Title Field (Optional)
157
+ **New Feature:**
158
+ - Optional title field for better identification
159
+ - Auto-generates from first 50 characters if left empty
160
+ - Makes browsing easier
161
+
162
+ **How to Use:**
163
+ 1. Leave empty for auto-generation
164
+ 2. OR enter custom title for important items
165
+
166
+ ---
167
+
168
+ ### 9. ✅ Better Statistics
169
+ **Improvements:**
170
+ - Now shows breakdown by content type
171
+ - Shows type icons
172
+ - More informative
173
+
174
+ ---
175
+
176
+ ## 🔧 All Fixed Issues
177
+
178
+ ### Fixed: Category/Section Renaming
179
+ - ✅ Can now rename without creating duplicates
180
+ - ✅ All related items update automatically
181
+ - ✅ No orphaned data
182
+
183
+ ### Fixed: Bulk Operations
184
+ - ✅ Can delete entire category
185
+ - ✅ Can delete entire section
186
+ - ✅ Confirmation prevents accidents
187
+
188
+ ### Fixed: Section Dropdown
189
+ - ✅ Filters by selected category
190
+ - ✅ Only shows relevant sections
191
+ - ✅ Much cleaner UX
192
+
193
+ ### Fixed: Search UX
194
+ - ✅ Auto-filtering (no search button)
195
+ - ✅ Instant feedback
196
+ - ✅ Better responsiveness
197
+
198
+ ### Fixed: Content Viewing
199
+ - ✅ Easy copy functionality
200
+ - ✅ Content preview panel
201
+ - ✅ Copy button for convenience
202
+
203
+ ### Fixed: Table Display
204
+ - ✅ Fixed height with scrolling
205
+ - ✅ Consistent interface size
206
+ - ✅ Better navigation
207
+
208
+ ---
209
+
210
+ ## 📊 Database Migration Details
211
+
212
+ ### What Happens Automatically:
213
+ 1. App detects old `prompts.db`
214
+ 2. Creates new `textarchive.db` schema
215
+ 3. Copies all data:
216
+ - `id` → `id`
217
+ - `category` → `category`
218
+ - `section` → `section`
219
+ - `prompt_text` → `content`
220
+ - First 50 chars → `title`
221
+ - Sets `content_type` to "prompt"
222
+ - Sets `file_extension` to ".txt"
223
+ - Preserves timestamps
224
+
225
+ ### Your Data is Safe:
226
+ - Old `prompts.db` is NOT deleted
227
+ - You can keep both files as backup
228
+ - All data is preserved
229
+
230
+ ---
231
+
232
+ ## 🎯 Comparison: Old vs New
233
+
234
+ | Feature | Old (Prompt Manager) | New (TextArchive) |
235
+ |---------|---------------------|-------------------|
236
+ | **Content Types** | Prompts only | 11 types (prompts, code, notes, etc.) |
237
+ | **Rename Category** | ❌ Creates duplicate | ✅ Updates all items |
238
+ | **Rename Section** | ❌ Creates duplicate | ✅ Updates all items |
239
+ | **Delete Category** | ❌ One by one only | ✅ Bulk delete with confirm |
240
+ | **Delete Section** | ❌ One by one only | ✅ Bulk delete with confirm |
241
+ | **Section Filter** | Shows all sections | ✅ Filters by category |
242
+ | **Search** | Click button | ✅ Auto-updates (AJAX) |
243
+ | **Copy Content** | Copy whole row | ✅ Copy button for text only |
244
+ | **Table Size** | Grows indefinitely | ✅ Fixed with scrolling |
245
+ | **Title Field** | ❌ Not available | ✅ Optional with auto-gen |
246
+ | **Statistics** | Basic | ✅ Enhanced with types |
247
+
248
+ ---
249
+
250
+ ## 🚀 Quick Start Guide
251
+
252
+ ### If You're New:
253
+ 1. Go to "Add New Item" tab
254
+ 2. Enter category, section, choose type
255
+ 3. Enter content
256
+ 4. Click "Add Item"
257
+ 5. Done!
258
+
259
+ ### If You're Migrating:
260
+ 1. Run the new app
261
+ 2. Your data automatically migrates
262
+ 3. Explore new "Manage Categories & Sections" tab
263
+ 4. Try the new auto-filtering
264
+ 5. Enjoy easy content copying!
265
+
266
+ ---
267
+
268
+ ## 💡 Pro Tips
269
+
270
+ ### Tip 1: Use Content Types
271
+ - Organize code separately from prompts
272
+ - Use "note" type for documentation
273
+ - Use proper types for better organization
274
+
275
+ ### Tip 2: Rename Instead of Recreate
276
+ - Use the rename feature in "Manage" tab
277
+ - Don't create new category/section with similar name
278
+ - Keeps data clean and organized
279
+
280
+ ### Tip 3: Use the Content Preview
281
+ - Click row to see full content
282
+ - Use copy button for quick copying
283
+ - No need to edit just to copy
284
+
285
+ ### Tip 4: Take Advantage of Auto-Filtering
286
+ - Set category filter
287
+ - Section dropdown automatically shows only relevant sections
288
+ - Quick way to navigate large collections
289
+
290
+ ### Tip 5: Regular Backups
291
+ - Periodically backup `textarchive.db`
292
+ - Keep old `prompts.db` as safety net
293
+ - Export important items
294
+
295
+ ---
296
+
297
+ ## ❓ FAQ
298
+
299
+ **Q: Will I lose my old data?**
300
+ A: No! The app automatically migrates your data from `prompts.db` to `textarchive.db`. Both files are kept.
301
+
302
+ **Q: Can I still use it just for prompts?**
303
+ A: Absolutely! Just always select "prompt" as content type. Works exactly like before, with bonus features.
304
+
305
+ **Q: What if I have duplicate category names?**
306
+ A: The new system handles case-sensitive duplicates better. Use the rename feature to consolidate.
307
+
308
+ **Q: Can I undo a bulk delete?**
309
+ A: No, that's why confirmation is required. Always backup your database before major deletions.
310
+
311
+ **Q: Do I need to click "Search" anymore?**
312
+ A: No! Filters update automatically. Just select your filters and the table updates instantly.
313
+
314
+ **Q: How do I copy just the text content?**
315
+ A: Click on a row → Content appears in preview → Click copy button (or select and Ctrl+C)
316
+
317
+ **Q: Can I have the same section name in different categories?**
318
+ A: Yes! Sections are independent. "General" can exist in both "Coding" and "Writing".
319
+
320
+ ---
321
+
322
+ ## 🎊 Conclusion
323
+
324
+ TextArchive is a major upgrade that:
325
+ - ✅ Fixes all critical issues
326
+ - ✅ Adds powerful new features
327
+ - ✅ Maintains backward compatibility
328
+ - ✅ Improves user experience significantly
329
+
330
+ Your old data is safe and automatically migrated. You can now manage text content of any type with powerful organization and management tools!
331
+
332
+ Enjoy your new TextArchive! 🚀
README.md CHANGED
@@ -1,78 +1,315 @@
1
- # TextArchive
 
 
 
 
 
 
 
 
 
2
 
3
- **True 3-way sync** between:
4
- - **Local**: E:\HuggingFace\TextArchive
5
- - **GitHub**: https://github.com/mnoorchenar/TextArchive
6
- - **Hugging Face**: https://huggingface.co/spaces/mnoorchenar/TextArchive
7
 
8
- ## Project Structure
9
 
10
- ```
11
- TextArchive/
12
- ├── templates/
13
- │ └── index.html
14
- ├── app.py
15
- ├── Dockerfile
16
- ├── requirements.txt
17
- ├── sync.ps1
18
- └── README.md
19
- ```
20
 
21
- ## Usage
 
 
 
 
 
 
 
 
22
 
23
- **Push changes (auto-merges from both remotes first):**
24
- ```powershell
25
- .\sync.ps1 "Your commit message"
26
- ```
27
 
28
- **Pull latest without pushing:**
29
- ```powershell
30
- .\sync.ps1 -PullOnly
31
- ```
 
32
 
33
- **Auto-sync (default message):**
34
- ```powershell
35
- .\sync.ps1
36
  ```
 
 
 
 
 
 
37
 
38
- **Debug mode:**
39
- ```powershell
40
- .\sync.ps1 -Verbose
 
41
  ```
42
 
43
- ## How It Works
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- This script performs **TRUE 3-way synchronization**:
 
 
 
46
 
47
- 1. **Fetches** from both GitHub and Hugging Face
48
- 2. **Merges** changes from BOTH remotes (not just the newest)
49
- 3. **Commits** your local changes
50
- 4. **Pushes** to both remotes
51
 
52
- ### Key Features
 
 
 
 
53
 
54
- **Never loses commits** - merges from both remotes instead of overwriting
55
- ✓ **Conflict handling** - keeps your local version if merge conflicts occur
56
- ✓ **Smart detection** - only pushes when there are actual changes
57
- ✓ **Status display** - shows timestamps from both remotes
 
58
 
59
- ### Example Workflow
 
 
 
 
60
 
61
- ```powershell
62
- # Someone updates GitHub, someone else updates HuggingFace
63
- # Run sync - it merges BOTH changes:
64
- .\sync.ps1 "My local changes"
 
65
 
66
- # Output:
67
- # Merged from GitHub
68
- # Merged from HuggingFace
69
- # Committed: My local changes
70
- # ✓ GitHub
71
- # Hugging Face
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  ```
73
 
74
- ## Important Notes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- ⚠️ **Git does NOT preserve file timestamps** - only commit timestamps are tracked
77
- ⚠️ Files will have current timestamp after pull/clone, not original creation date
78
- ⚠️ If you need file metadata preserved, consider using rsync instead
 
1
+ ---
2
+ title: TextArchive
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ python_version: "3.11"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
 
12
+ # TextArchive - Universal Text Management System
 
 
 
13
 
14
+ A powerful and intuitive web application for managing all your text-based content with a clear hierarchical structure: **Category → Section → Content Type → Content**.
15
 
16
+ ## 🎯 What is TextArchive?
 
 
 
 
 
 
 
 
 
17
 
18
+ TextArchive is not just a prompt manager - it's a universal system for organizing ANY text-based content:
19
+ - 💬 Prompts for AI models
20
+ - 📝 Notes and documentation
21
+ - 🐍 Code snippets (Python, JavaScript, SQL, etc.)
22
+ - 📐 LaTeX documents
23
+ - 💻 Bash scripts
24
+ - ⚙️ Functions and utilities
25
+ - 🌐 HTML/CSS templates
26
+ - 📄 Any other text content
27
 
28
+ ## 📐 Organization Structure
 
 
 
29
 
30
+ Your content is organized in a clear three-level hierarchy:
31
+ - **Category** (High-level grouping) - e.g., "Coding", "Writing", "Research"
32
+ - **Section** (Subsection under category) - e.g., "Python", "JavaScript", "Blog Posts"
33
+ - **Content Type** (What kind of content) - e.g., "python", "note", "prompt", "latex"
34
+ - **Content** (Your actual text)
35
 
36
+ Example hierarchy:
 
 
37
  ```
38
+ 📁 Coding (Category)
39
+ └── 📄 Python (Section)
40
+ └── 🐍 Python Code: "Function to calculate fibonacci..."
41
+ └── 💬 Prompt: "Write a Python function that..."
42
+ └── 📄 JavaScript (Section)
43
+ └── 📜 JavaScript: "React component for login..."
44
 
45
+ 📁 Research (Category)
46
+ └── 📄 AI Papers (Section)
47
+ └── 📝 Note: "Summary of transformer architecture..."
48
+ └── 📐 LaTeX: "\documentclass{article}..."
49
  ```
50
 
51
+ ## Features
52
+
53
+ ### Core Functionality
54
+ - ✅ **Add New Items**: Create content with custom categories, sections, and types
55
+ - 📋 **View All Items**: See all your content organized hierarchically
56
+ - 🔍 **Auto-Filter**: Filters update automatically (AJAX-style, no search button needed)
57
+ - 📄 **Easy Copy**: Click any row to preview, then use the copy button to copy just the text content
58
+ - ✏️ **Edit Items**: Click any item to edit its details
59
+ - 🗑️ **Delete Items**: Remove items you no longer need
60
+ - 🎯 **Fixed Height Table**: Scrollable table maintains consistent size
61
+ - 💾 **Persistent Storage**: SQLite database stores all your data
62
+
63
+ ### Advanced Management
64
+ - 🔄 **Rename Categories**: Rename categories and automatically update all items
65
+ - 🔄 **Rename Sections**: Rename sections across all categories or within a specific category
66
+ - 🗑️ **Bulk Delete**: Delete entire categories or sections at once (with confirmation)
67
+ - 📊 **Statistics**: View insights about your content collection
68
+ - 🏷️ **11 Content Types**: Prompt, Note, Python, JavaScript, LaTeX, SQL, Bash, Function, HTML, CSS, Other
69
+
70
+ ### Smart UX
71
+ - 🎯 **Filtered Dropdowns**: Section dropdown automatically filters by selected category
72
+ - 🔄 **Auto-Refresh**: All dropdowns update automatically after changes
73
+ - ⚠️ **Confirmation Dialogs**: Destructive actions require confirmation
74
+ - 📦 **Auto-Generated Titles**: Leave title empty to auto-generate from content
75
+ - 🎨 **Type Icons**: Visual icons for each content type
76
+
77
+ ## 🚀 How to Use
78
+
79
+ ### Adding Content
80
+ 1. Go to the "➕ Add New Item" tab
81
+ 2. Select or type a **Category** (high-level grouping)
82
+ 3. Select or type a **Section** (subsection - dropdown auto-filters by category)
83
+ 4. Choose **Content Type** (prompt, note, python, etc.)
84
+ 5. (Optional) Enter a **Title** or leave empty for auto-generation
85
+ 6. Enter your **Content**
86
+ 7. Click "Add Item"
87
+
88
+ ### Viewing and Filtering Content
89
+ 1. Go to the "📋 View & Manage Items" tab
90
+ 2. Use filters to narrow down items:
91
+ - **Filter by Category**: Auto-updates table
92
+ - **Filter by Section**: Auto-updates table (filtered by category)
93
+ - **Filter by Type**: Auto-updates table
94
+ 3. Click "Clear Filters" to reset
95
+
96
+ ### Copying Content
97
+ 1. In the "View & Manage Items" tab, click on any row in the table
98
+ 2. The content appears in the **Content Preview** box below
99
+ 3. Click the **copy button** (📋) in the preview box, OR
100
+ 4. Click in the text box and press Ctrl+A (select all), then Ctrl+C (copy)
101
+ 5. Only the text content is copied - NO ID, category, or section included!
102
+
103
+ ### Editing Content
104
+ 1. Click on a row in the table to load it
105
+ 2. The item loads in the editing section below
106
+ 3. Modify Category, Section, Type, Title, or Content as needed
107
+ 4. Click "💾 Update Item" to save changes
108
 
109
+ ### Deleting Content
110
+ 1. Click on a row to select it
111
+ 2. Click the "🗑️ Delete Item" button
112
+ 3. The item will be removed immediately
113
 
114
+ ### Managing Categories & Sections
115
+ 1. Go to the "⚙️ Manage Categories & Sections" tab
 
 
116
 
117
+ **Rename Category:**
118
+ - Select the category to rename
119
+ - Enter new name
120
+ - Click "🔄 Rename Category"
121
+ - All items in that category will be updated
122
 
123
+ **Rename Section:**
124
+ - (Optional) Select a category to limit scope
125
+ - Select the section to rename
126
+ - Enter new name
127
+ - Click "🔄 Rename Section"
128
 
129
+ **Delete Category:**
130
+ - Select the category
131
+ - Check the confirmation box
132
+ - Click "🗑️ Delete Category"
133
+ - ALL items in that category will be deleted
134
 
135
+ **Delete Section:**
136
+ - (Optional) Select a category to limit scope
137
+ - Select the section
138
+ - Check the confirmation box
139
+ - Click "🗑️ Delete Section"
140
 
141
+ ### Viewing Statistics
142
+ 1. Go to the "📊 Statistics" tab
143
+ 2. View total counts, top categories, and breakdown by type
144
+ 3. Click "🔄 Refresh Statistics" to update
145
+
146
+ ## 🎨 Content Types
147
+
148
+ TextArchive supports 11 content types, each with its own icon and file extension:
149
+
150
+ | Type | Icon | Extension | Use For |
151
+ |------|------|-----------|---------|
152
+ | prompt | 💬 | .txt | AI prompts and instructions |
153
+ | note | 📝 | .md | Notes and documentation |
154
+ | python | 🐍 | .py | Python code and scripts |
155
+ | javascript | 📜 | .js | JavaScript code |
156
+ | latex | 📐 | .tex | LaTeX documents |
157
+ | sql | 🗄️ | .sql | SQL queries and scripts |
158
+ | bash | 💻 | .sh | Bash shell scripts |
159
+ | function | ⚙️ | .txt | Reusable functions |
160
+ | html | 🌐 | .html | HTML markup |
161
+ | css | 🎨 | .css | CSS stylesheets |
162
+ | other | 📄 | .txt | Any other text content |
163
+
164
+ ## 🔧 Technical Details
165
+
166
+ - **Framework**: Gradio 4.44.0+
167
+ - **Database**: SQLite (automatically created as `textarchive.db`)
168
+ - **Python**: 3.8+
169
+ - **Auto-Migration**: Automatically migrates from old `prompts.db` if exists
170
+
171
+ ## 📊 Database Schema
172
+
173
+ ```sql
174
+ CREATE TABLE text_items (
175
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
176
+ category TEXT NOT NULL,
177
+ section TEXT NOT NULL,
178
+ title TEXT NOT NULL,
179
+ content TEXT NOT NULL,
180
+ content_type TEXT DEFAULT 'prompt',
181
+ file_extension TEXT DEFAULT '.txt',
182
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
183
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
184
+ );
185
  ```
186
 
187
+ Indexes are created on `category`, `section`, and `content_type` for optimal performance.
188
+
189
+ ## 🚀 Deployment to Hugging Face Spaces
190
+
191
+ ### Method 1: Direct Upload
192
+
193
+ 1. Create a new Space on Hugging Face:
194
+ - Go to https://huggingface.co/new-space
195
+ - Choose "Gradio" as the SDK
196
+ - Set visibility (Public or Private)
197
+
198
+ 2. Upload these files to your Space:
199
+ - `app.py`
200
+ - `requirements.txt`
201
+ - `README.md` (optional)
202
+
203
+ 3. Your app will automatically deploy!
204
+
205
+ ### Method 2: Git (With Your Sync Script)
206
+
207
+ 1. Initialize your local repository:
208
+ ```bash
209
+ git init
210
+ git add .
211
+ git commit -m "Initial commit"
212
+ ```
213
+
214
+ 2. Add remotes:
215
+ ```bash
216
+ # Add GitHub remote
217
+ git remote add github https://github.com/YOUR_USERNAME/textarchive.git
218
+
219
+ # Add Hugging Face remote
220
+ git remote add huggingface https://huggingface.co/spaces/YOUR_USERNAME/textarchive
221
+ ```
222
+
223
+ 3. Use your sync script:
224
+ ```powershell
225
+ # Set your Hugging Face token
226
+ $env:HF_TOKEN = "your_hf_token_here"
227
+
228
+ # Run sync
229
+ .\sync.ps1 "Initial deployment"
230
+ ```
231
+
232
+ ## 🔄 Migration from Old "Prompt Manager"
233
+
234
+ If you have an existing `prompts.db` from the old Prompt Manager:
235
+
236
+ 1. **Automatic Migration**: Just run the new app - it will automatically:
237
+ - Detect the old `prompts.db`
238
+ - Create the new `textarchive.db` schema
239
+ - Migrate all your data
240
+ - Set content_type to "prompt" for all old items
241
+ - Generate titles from the first 50 characters
242
+
243
+ 2. **Manual Backup** (Recommended):
244
+ ```bash
245
+ # Backup your old database first
246
+ cp prompts.db prompts.db.backup
247
+ ```
248
+
249
+ 3. **Verify Migration**:
250
+ - Open the app
251
+ - Go to Statistics tab
252
+ - Check that all items were migrated
253
+ - Check a few items to ensure content is intact
254
+
255
+ ## 📝 Notes
256
+
257
+ - The database file persists between sessions
258
+ - All users share the same database (unless you add authentication)
259
+ - Backup your `textarchive.db` file regularly
260
+ - Section dropdowns auto-filter by selected category for better UX
261
+ - Auto-filtering eliminates the need for a search button
262
+ - Fixed-height table with scrolling maintains clean interface
263
+
264
+ ## 🎯 Use Cases
265
+
266
+ ### For Developers
267
+ - Store code snippets and functions
268
+ - Organize SQL queries by project
269
+ - Keep bash scripts and automation tools
270
+ - Document API endpoints
271
+
272
+ ### For Writers
273
+ - Organize story prompts and ideas
274
+ - Store article outlines
275
+ - Keep writing templates
276
+ - Manage blog post drafts
277
+
278
+ ### For Researchers
279
+ - Store LaTeX document snippets
280
+ - Organize research notes by topic
281
+ - Keep citation templates
282
+ - Document methodologies
283
+
284
+ ### For AI/ML Engineers
285
+ - Organize prompts by use case
286
+ - Store fine-tuning examples
287
+ - Document model configurations
288
+ - Keep evaluation templates
289
+
290
+ ## 🔮 Future Enhancements (Suggestions)
291
+
292
+ - Export individual items as files
293
+ - Export category/section as ZIP
294
+ - Import from files/folders
295
+ - Tags system for cross-category organization
296
+ - Full-text search within content
297
+ - Syntax highlighting in preview
298
+ - Markdown rendering for notes
299
+ - Version history for items
300
+ - User authentication for private libraries
301
+ - API access for programmatic usage
302
+ - Favorites/bookmarks
303
+ - Sharing capabilities
304
+
305
+ ## 📜 License
306
+
307
+ MIT License - Feel free to modify and use as needed!
308
+
309
+ ## 🤝 Contributing
310
+
311
+ Contributions are welcome! Please feel free to submit issues or pull requests.
312
+
313
+ ---
314
 
315
+ **Built with ❤️ using Gradio and SQLite**
 
 
app.py CHANGED
@@ -1,10 +1,818 @@
1
- from flask import Flask, render_template
 
 
 
 
 
 
2
 
3
- app = Flask(__name__)
 
4
 
5
- @app.route('/')
6
- def home():
7
- return render_template('index.html')
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- if __name__ == '__main__':
10
- app.run(host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TextArchive - Universal Text Management System
2
+ import gradio as gr
3
+ import sqlite3
4
+ import pandas as pd
5
+ from datetime import datetime
6
+ import os
7
+ import json
8
 
9
+ # Database setup
10
+ DB_PATH = "textarchive.db"
11
 
12
+ # Content types supported
13
+ CONTENT_TYPES = {
14
+ "prompt": {"extension": ".txt", "icon": "💬", "label": "Prompt"},
15
+ "note": {"extension": ".md", "icon": "📝", "label": "Note"},
16
+ "python": {"extension": ".py", "icon": "🐍", "label": "Python Code"},
17
+ "javascript": {"extension": ".js", "icon": "📜", "label": "JavaScript"},
18
+ "latex": {"extension": ".tex", "icon": "📐", "label": "LaTeX"},
19
+ "sql": {"extension": ".sql", "icon": "🗄️", "label": "SQL"},
20
+ "bash": {"extension": ".sh", "icon": "💻", "label": "Bash Script"},
21
+ "function": {"extension": ".txt", "icon": "⚙️", "label": "Function"},
22
+ "html": {"extension": ".html", "icon": "🌐", "label": "HTML"},
23
+ "css": {"extension": ".css", "icon": "🎨", "label": "CSS"},
24
+ "other": {"extension": ".txt", "icon": "📄", "label": "Other"}
25
+ }
26
 
27
+ def init_db():
28
+ """Initialize the database with text_items table"""
29
+ conn = sqlite3.connect(DB_PATH)
30
+ cursor = conn.cursor()
31
+
32
+ # Check if old table exists and migrate
33
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='prompts'")
34
+ old_table_exists = cursor.fetchone() is not None
35
+
36
+ # Create new table
37
+ cursor.execute('''
38
+ CREATE TABLE IF NOT EXISTS text_items (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ category TEXT NOT NULL,
41
+ section TEXT NOT NULL,
42
+ title TEXT NOT NULL,
43
+ content TEXT NOT NULL,
44
+ content_type TEXT DEFAULT 'prompt',
45
+ file_extension TEXT DEFAULT '.txt',
46
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
48
+ )
49
+ ''')
50
+
51
+ # Migrate data from old table if exists
52
+ if old_table_exists:
53
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='text_items'")
54
+ new_table_exists = cursor.fetchone() is not None
55
+
56
+ if new_table_exists:
57
+ cursor.execute("SELECT COUNT(*) FROM text_items")
58
+ if cursor.fetchone()[0] == 0: # Only migrate if new table is empty
59
+ cursor.execute('''
60
+ INSERT INTO text_items (id, category, section, title, content, content_type, file_extension, created_at, updated_at)
61
+ SELECT id, category, section,
62
+ substr(prompt_text, 1, 50) as title,
63
+ prompt_text as content,
64
+ 'prompt' as content_type,
65
+ '.txt' as file_extension,
66
+ created_at, updated_at
67
+ FROM prompts
68
+ ''')
69
+
70
+ # Create indexes
71
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_category ON text_items(category)")
72
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_section ON text_items(section)")
73
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_content_type ON text_items(content_type)")
74
+
75
+ conn.commit()
76
+ conn.close()
77
+
78
+ def get_unique_categories():
79
+ """Get all unique categories from the database"""
80
+ conn = sqlite3.connect(DB_PATH)
81
+ cursor = conn.cursor()
82
+ cursor.execute("SELECT DISTINCT category FROM text_items ORDER BY category")
83
+ categories = [row[0] for row in cursor.fetchall()]
84
+ conn.close()
85
+ return categories
86
+
87
+ def get_sections_by_category(category):
88
+ """Get sections for a specific category"""
89
+ if not category or category.strip() == "":
90
+ return []
91
+ conn = sqlite3.connect(DB_PATH)
92
+ cursor = conn.cursor()
93
+ cursor.execute("SELECT DISTINCT section FROM text_items WHERE category=? ORDER BY section", (category,))
94
+ sections = [row[0] for row in cursor.fetchall()]
95
+ conn.close()
96
+ return sections
97
+
98
+ def get_all_unique_sections():
99
+ """Get all unique sections from the database"""
100
+ conn = sqlite3.connect(DB_PATH)
101
+ cursor = conn.cursor()
102
+ cursor.execute("SELECT DISTINCT section FROM text_items ORDER BY section")
103
+ sections = [row[0] for row in cursor.fetchall()]
104
+ conn.close()
105
+ return sections
106
+
107
+ def add_text_item(category, section, title, content, content_type):
108
+ """Add a new text item to the database"""
109
+ if not category or not section or not content:
110
+ return "❌ Error: Category, Section, and Content are required!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=[])
111
+
112
+ # Use first 50 chars of content as title if title is empty
113
+ if not title or title.strip() == "":
114
+ title = content.strip()[:50] + ("..." if len(content.strip()) > 50 else "")
115
+
116
+ # Get file extension from content type
117
+ file_ext = CONTENT_TYPES.get(content_type, CONTENT_TYPES["other"])["extension"]
118
+
119
+ conn = sqlite3.connect(DB_PATH)
120
+ cursor = conn.cursor()
121
+ cursor.execute(
122
+ "INSERT INTO text_items (category, section, title, content, content_type, file_extension) VALUES (?, ?, ?, ?, ?, ?)",
123
+ (category.strip(), section.strip(), title.strip(), content.strip(), content_type, file_ext)
124
+ )
125
+ conn.commit()
126
+ conn.close()
127
+
128
+ return "✅ Item added successfully!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=get_sections_by_category(category))
129
+
130
+ def search_text_items(category_filter, section_filter, type_filter):
131
+ """Search text items by category, section, and/or content type"""
132
+ conn = sqlite3.connect(DB_PATH)
133
+
134
+ query = "SELECT id, category, section, title, content, content_type, created_at, updated_at FROM text_items WHERE 1=1"
135
+ params = []
136
+
137
+ if category_filter and category_filter.strip():
138
+ query += " AND category = ?"
139
+ params.append(category_filter.strip())
140
+
141
+ if section_filter and section_filter.strip():
142
+ query += " AND section = ?"
143
+ params.append(section_filter.strip())
144
+
145
+ if type_filter and type_filter.strip() and type_filter != "All Types":
146
+ query += " AND content_type = ?"
147
+ params.append(type_filter.strip())
148
+
149
+ query += " ORDER BY category, section, updated_at DESC"
150
+
151
+ df = pd.read_sql_query(query, conn, params=params)
152
+ conn.close()
153
+
154
+ # Add icon to content type for display
155
+ if not df.empty:
156
+ df['Type'] = df['content_type'].apply(lambda x: f"{CONTENT_TYPES.get(x, CONTENT_TYPES['other'])['icon']} {CONTENT_TYPES.get(x, CONTENT_TYPES['other'])['label']}")
157
+ # Reorder columns for better display (hide content in table view)
158
+ df = df[['id', 'category', 'section', 'Type', 'title', 'created_at', 'updated_at']]
159
+
160
+ return df
161
+
162
+ def get_content_by_id(item_id):
163
+ """Get the full content of a specific item by ID"""
164
+ conn = sqlite3.connect(DB_PATH)
165
+ cursor = conn.cursor()
166
+ cursor.execute("SELECT content FROM text_items WHERE id=?", (item_id,))
167
+ result = cursor.fetchone()
168
+ conn.close()
169
+ return result[0] if result else ""
170
+
171
+ def update_text_item(item_id, category, section, title, content, content_type):
172
+ """Update an existing text item"""
173
+ if not item_id:
174
+ return "❌ Error: Please select an item to update!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=[])
175
+
176
+ if not category or not section or not content:
177
+ return "❌ Error: Category, Section, and Content are required!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=[])
178
+
179
+ # Use first 50 chars of content as title if title is empty
180
+ if not title or title.strip() == "":
181
+ title = content.strip()[:50] + ("..." if len(content.strip()) > 50 else "")
182
+
183
+ # Get file extension from content type
184
+ file_ext = CONTENT_TYPES.get(content_type, CONTENT_TYPES["other"])["extension"]
185
+
186
+ conn = sqlite3.connect(DB_PATH)
187
+ cursor = conn.cursor()
188
+ cursor.execute(
189
+ "UPDATE text_items SET category=?, section=?, title=?, content=?, content_type=?, file_extension=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
190
+ (category.strip(), section.strip(), title.strip(), content.strip(), content_type, file_ext, item_id)
191
+ )
192
+ conn.commit()
193
+ conn.close()
194
+ return "✅ Item updated successfully!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=get_sections_by_category(category))
195
+
196
+ def delete_text_item(item_id):
197
+ """Delete a text item from the database"""
198
+ if not item_id:
199
+ return "❌ Error: Please select an item to delete!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=[])
200
+
201
+ conn = sqlite3.connect(DB_PATH)
202
+ cursor = conn.cursor()
203
+ cursor.execute("DELETE FROM text_items WHERE id=?", (item_id,))
204
+ conn.commit()
205
+ conn.close()
206
+ return "✅ Item deleted successfully!", search_text_items("", "", ""), gr.Dropdown(choices=get_unique_categories()), gr.Dropdown(choices=[])
207
+
208
+ def load_item_for_editing(items_df, evt: gr.SelectData):
209
+ """Load selected item data for editing"""
210
+ if items_df is None or items_df.empty:
211
+ return None, "", "", "", "", "prompt"
212
+
213
+ row_index = evt.index[0]
214
+ row = items_df.iloc[row_index]
215
+ item_id = row['id']
216
+
217
+ # Get full content from database
218
+ content = get_content_by_id(item_id)
219
+
220
+ # Extract content_type from Type column (remove icon)
221
+ type_display = row['Type']
222
+ content_type = next((k for k, v in CONTENT_TYPES.items() if v['label'] in type_display), 'other')
223
+
224
+ return item_id, row['category'], row['section'], row['title'], content, content_type
225
+
226
+ def rename_category(old_name, new_name):
227
+ """Rename a category (updates all items with that category)"""
228
+ if not old_name or not new_name:
229
+ return "❌ Error: Both old and new category names are required!", search_text_items("", "", "")
230
+
231
+ if old_name.strip() == new_name.strip():
232
+ return "❌ Error: New name must be different from old name!", search_text_items("", "", "")
233
+
234
+ conn = sqlite3.connect(DB_PATH)
235
+ cursor = conn.cursor()
236
+
237
+ # Check how many items will be affected
238
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE category=?", (old_name.strip(),))
239
+ count = cursor.fetchone()[0]
240
+
241
+ if count == 0:
242
+ conn.close()
243
+ return f"❌ Error: Category '{old_name}' not found!", search_text_items("", "", "")
244
+
245
+ # Perform the rename
246
+ cursor.execute(
247
+ "UPDATE text_items SET category=?, updated_at=CURRENT_TIMESTAMP WHERE category=?",
248
+ (new_name.strip(), old_name.strip())
249
+ )
250
+ conn.commit()
251
+ conn.close()
252
+
253
+ return f"✅ Category renamed! {count} items updated from '{old_name}' to '{new_name}'", search_text_items("", "", "")
254
+
255
+ def rename_section(category, old_name, new_name):
256
+ """Rename a section within a category (updates all items with that section)"""
257
+ if not old_name or not new_name:
258
+ return "❌ Error: Both old and new section names are required!", search_text_items("", "", "")
259
+
260
+ if old_name.strip() == new_name.strip():
261
+ return "❌ Error: New name must be different from old name!", search_text_items("", "", "")
262
+
263
+ conn = sqlite3.connect(DB_PATH)
264
+ cursor = conn.cursor()
265
+
266
+ # Build query based on whether category is specified
267
+ if category and category.strip():
268
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE category=? AND section=?",
269
+ (category.strip(), old_name.strip()))
270
+ count = cursor.fetchone()[0]
271
+
272
+ if count == 0:
273
+ conn.close()
274
+ return f"❌ Error: Section '{old_name}' not found in category '{category}'!", search_text_items("", "", "")
275
+
276
+ cursor.execute(
277
+ "UPDATE text_items SET section=?, updated_at=CURRENT_TIMESTAMP WHERE category=? AND section=?",
278
+ (new_name.strip(), category.strip(), old_name.strip())
279
+ )
280
+ else:
281
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE section=?", (old_name.strip(),))
282
+ count = cursor.fetchone()[0]
283
+
284
+ if count == 0:
285
+ conn.close()
286
+ return f"❌ Error: Section '{old_name}' not found!", search_text_items("", "", "")
287
+
288
+ cursor.execute(
289
+ "UPDATE text_items SET section=?, updated_at=CURRENT_TIMESTAMP WHERE section=?",
290
+ (new_name.strip(), old_name.strip())
291
+ )
292
+
293
+ conn.commit()
294
+ conn.close()
295
+
296
+ return f"✅ Section renamed! {count} items updated from '{old_name}' to '{new_name}'", search_text_items("", "", "")
297
+
298
+ def delete_by_category(category, confirm=False):
299
+ """Delete all items in a category"""
300
+ if not category or not category.strip():
301
+ return "❌ Error: Category is required!", search_text_items("", "", "")
302
+
303
+ conn = sqlite3.connect(DB_PATH)
304
+ cursor = conn.cursor()
305
+
306
+ # Get count
307
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE category=?", (category.strip(),))
308
+ count = cursor.fetchone()[0]
309
+
310
+ if count == 0:
311
+ conn.close()
312
+ return f"❌ Error: Category '{category}' not found or is empty!", search_text_items("", "", "")
313
+
314
+ if not confirm:
315
+ conn.close()
316
+ return f"⚠️ Warning: This will delete {count} items in category '{category}'. Click again to confirm!", search_text_items("", "", "")
317
+
318
+ # Perform deletion
319
+ cursor.execute("DELETE FROM text_items WHERE category=?", (category.strip(),))
320
+ conn.commit()
321
+ conn.close()
322
+
323
+ return f"✅ Category deleted! {count} items removed from '{category}'", search_text_items("", "", "")
324
+
325
+ def delete_by_section(category, section, confirm=False):
326
+ """Delete all items in a section"""
327
+ if not section or not section.strip():
328
+ return "❌ Error: Section is required!", search_text_items("", "", "")
329
+
330
+ conn = sqlite3.connect(DB_PATH)
331
+ cursor = conn.cursor()
332
+
333
+ # Build query based on whether category is specified
334
+ if category and category.strip():
335
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE category=? AND section=?",
336
+ (category.strip(), section.strip()))
337
+ count = cursor.fetchone()[0]
338
+
339
+ if count == 0:
340
+ conn.close()
341
+ return f"❌ Error: Section '{section}' not found in category '{category}' or is empty!", search_text_items("", "", "")
342
+
343
+ if not confirm:
344
+ conn.close()
345
+ return f"⚠️ Warning: This will delete {count} items in section '{section}' under '{category}'. Click again to confirm!", search_text_items("", "", "")
346
+
347
+ cursor.execute("DELETE FROM text_items WHERE category=? AND section=?",
348
+ (category.strip(), section.strip()))
349
+ else:
350
+ cursor.execute("SELECT COUNT(*) FROM text_items WHERE section=?", (section.strip(),))
351
+ count = cursor.fetchone()[0]
352
+
353
+ if count == 0:
354
+ conn.close()
355
+ return f"❌ Error: Section '{section}' not found or is empty!", search_text_items("", "", "")
356
+
357
+ if not confirm:
358
+ conn.close()
359
+ return f"⚠️ Warning: This will delete {count} items in section '{section}' (across all categories). Click again to confirm!", search_text_items("", "", "")
360
+
361
+ cursor.execute("DELETE FROM text_items WHERE section=?", (section.strip(),))
362
+
363
+ conn.commit()
364
+ conn.close()
365
+
366
+ return f"✅ Section deleted! {count} items removed from '{section}'", search_text_items("", "", "")
367
+
368
+ # Initialize database
369
+ init_db()
370
+
371
+ # Create Gradio interface
372
+ with gr.Blocks(title="TextArchive", theme=gr.themes.Soft()) as app:
373
+ gr.Markdown("# 📚 TextArchive - Universal Text Management System")
374
+ gr.Markdown("Organize your text content: **Category** (high-level) → **Section** (subsection) → **Content**")
375
+
376
+ with gr.Tabs():
377
+ # Add New Item Tab
378
+ with gr.Tab("➕ Add New Item"):
379
+ gr.Markdown("### Add a New Text Item")
380
+ gr.Markdown("💡 **Hierarchy**: Category → Section → Content Type → Content")
381
+ with gr.Row():
382
+ with gr.Column():
383
+ new_category = gr.Dropdown(
384
+ label="Category (High-Level)",
385
+ choices=get_unique_categories(),
386
+ allow_custom_value=True,
387
+ info="Enter a new category or select existing"
388
+ )
389
+ new_section = gr.Dropdown(
390
+ label="Section (Subsection)",
391
+ choices=[],
392
+ allow_custom_value=True,
393
+ info="Enter a new section or select existing"
394
+ )
395
+ new_content_type = gr.Dropdown(
396
+ label="Content Type",
397
+ choices=list(CONTENT_TYPES.keys()),
398
+ value="prompt",
399
+ info="Select the type of content"
400
+ )
401
+ new_title = gr.Textbox(
402
+ label="Title (Optional)",
403
+ placeholder="Leave empty to auto-generate from content...",
404
+ lines=1
405
+ )
406
+ new_content = gr.Textbox(
407
+ label="Content",
408
+ placeholder="Enter your content here...",
409
+ lines=12
410
+ )
411
+ add_btn = gr.Button("➕ Add Item", variant="primary", size="lg")
412
+ add_status = gr.Textbox(label="Status", interactive=False)
413
+
414
+ # View & Manage Items Tab
415
+ with gr.Tab("📋 View & Manage Items"):
416
+ with gr.Row():
417
+ with gr.Column(scale=1):
418
+ gr.Markdown("### 🔍 Filter (Auto-updates)")
419
+ filter_category = gr.Dropdown(
420
+ label="Filter by Category",
421
+ choices=[""] + get_unique_categories(),
422
+ value="",
423
+ allow_custom_value=False,
424
+ info="Select category to filter"
425
+ )
426
+ filter_section = gr.Dropdown(
427
+ label="Filter by Section",
428
+ choices=[""],
429
+ value="",
430
+ allow_custom_value=False,
431
+ info="Select section to filter"
432
+ )
433
+ filter_type = gr.Dropdown(
434
+ label="Filter by Type",
435
+ choices=["All Types"] + list(CONTENT_TYPES.keys()),
436
+ value="All Types",
437
+ info="Select content type"
438
+ )
439
+ clear_filter_btn = gr.Button("🔄 Clear Filters", variant="secondary")
440
+
441
+ with gr.Column(scale=3):
442
+ gr.Markdown("### All Items (Click row to view/edit)")
443
+ items_display = gr.Dataframe(
444
+ value=search_text_items("", "", ""),
445
+ label="Text Items - Category → Section → Type → Title",
446
+ interactive=False,
447
+ wrap=True,
448
+ height=400, # Fixed height with scrolling
449
+ column_widths=["5%", "15%", "15%", "15%", "35%", "15%"]
450
+ )
451
+
452
+ gr.Markdown("---")
453
+
454
+ # Content Preview and Copy Section
455
+ with gr.Row():
456
+ with gr.Column():
457
+ gr.Markdown("### 📄 Content Preview")
458
+ content_preview = gr.Textbox(
459
+ label="Selected Content (Click to select all, then Ctrl+C to copy)",
460
+ lines=8,
461
+ interactive=True,
462
+ show_copy_button=True
463
+ )
464
+
465
+ gr.Markdown("---")
466
+ gr.Markdown("### ✏️ Edit Selected Item")
467
+
468
+ with gr.Row():
469
+ with gr.Column():
470
+ edit_id = gr.Number(label="Item ID", visible=False)
471
+ edit_category = gr.Dropdown(
472
+ label="Category (High-Level)",
473
+ choices=get_unique_categories(),
474
+ allow_custom_value=True
475
+ )
476
+ edit_section = gr.Dropdown(
477
+ label="Section (Subsection)",
478
+ choices=[],
479
+ allow_custom_value=True
480
+ )
481
+ edit_content_type = gr.Dropdown(
482
+ label="Content Type",
483
+ choices=list(CONTENT_TYPES.keys()),
484
+ value="prompt"
485
+ )
486
+ edit_title = gr.Textbox(label="Title", lines=1)
487
+ edit_content = gr.Textbox(label="Content", lines=10)
488
+
489
+ with gr.Row():
490
+ update_btn = gr.Button("💾 Update Item", variant="primary")
491
+ delete_btn = gr.Button("🗑️ Delete Item", variant="stop")
492
+
493
+ edit_status = gr.Textbox(label="Status", interactive=False)
494
+
495
+ # Manage Categories & Sections Tab
496
+ with gr.Tab("⚙️ Manage Categories & Sections"):
497
+ gr.Markdown("### 🏷️ Rename or Delete Categories and Sections")
498
+
499
+ with gr.Row():
500
+ # Rename Category
501
+ with gr.Column():
502
+ gr.Markdown("#### Rename Category")
503
+ rename_cat_old = gr.Dropdown(
504
+ label="Select Category",
505
+ choices=get_unique_categories(),
506
+ allow_custom_value=False
507
+ )
508
+ rename_cat_new = gr.Textbox(
509
+ label="New Category Name",
510
+ placeholder="Enter new name..."
511
+ )
512
+ rename_cat_btn = gr.Button("🔄 Rename Category", variant="primary")
513
+ rename_cat_status = gr.Textbox(label="Status", interactive=False)
514
+
515
+ # Rename Section
516
+ with gr.Column():
517
+ gr.Markdown("#### Rename Section")
518
+ rename_sec_category = gr.Dropdown(
519
+ label="Category (Optional)",
520
+ choices=[""] + get_unique_categories(),
521
+ allow_custom_value=False,
522
+ info="Leave empty to rename across all categories"
523
+ )
524
+ rename_sec_old = gr.Dropdown(
525
+ label="Select Section",
526
+ choices=get_all_unique_sections(),
527
+ allow_custom_value=False
528
+ )
529
+ rename_sec_new = gr.Textbox(
530
+ label="New Section Name",
531
+ placeholder="Enter new name..."
532
+ )
533
+ rename_sec_btn = gr.Button("🔄 Rename Section", variant="primary")
534
+ rename_sec_status = gr.Textbox(label="Status", interactive=False)
535
+
536
+ gr.Markdown("---")
537
+
538
+ with gr.Row():
539
+ # Delete Category
540
+ with gr.Column():
541
+ gr.Markdown("#### Delete Entire Category")
542
+ gr.Markdown("⚠️ **Warning**: This will delete ALL items in the category!")
543
+ delete_cat_select = gr.Dropdown(
544
+ label="Select Category",
545
+ choices=get_unique_categories(),
546
+ allow_custom_value=False
547
+ )
548
+ delete_cat_confirm = gr.Checkbox(label="I understand this will delete all items", value=False)
549
+ delete_cat_btn = gr.Button("🗑️ Delete Category", variant="stop")
550
+ delete_cat_status = gr.Textbox(label="Status", interactive=False)
551
+
552
+ # Delete Section
553
+ with gr.Column():
554
+ gr.Markdown("#### Delete Entire Section")
555
+ gr.Markdown("⚠️ **Warning**: This will delete ALL items in the section!")
556
+ delete_sec_category = gr.Dropdown(
557
+ label="Category (Optional)",
558
+ choices=[""] + get_unique_categories(),
559
+ allow_custom_value=False,
560
+ info="Leave empty to delete across all categories"
561
+ )
562
+ delete_sec_select = gr.Dropdown(
563
+ label="Select Section",
564
+ choices=get_all_unique_sections(),
565
+ allow_custom_value=False
566
+ )
567
+ delete_sec_confirm = gr.Checkbox(label="I understand this will delete all items", value=False)
568
+ delete_sec_btn = gr.Button("🗑️ Delete Section", variant="stop")
569
+ delete_sec_status = gr.Textbox(label="Status", interactive=False)
570
+
571
+ # Statistics Tab
572
+ with gr.Tab("📊 Statistics"):
573
+ stats_display = gr.Markdown()
574
+ refresh_stats_btn = gr.Button("🔄 Refresh Statistics", variant="secondary")
575
+
576
+ # Event handlers
577
+ def clear_filters():
578
+ return "", "", "All Types", search_text_items("", "", "")
579
+
580
+ def get_statistics():
581
+ conn = sqlite3.connect(DB_PATH)
582
+ cursor = conn.cursor()
583
+
584
+ cursor.execute("SELECT COUNT(*) FROM text_items")
585
+ total = cursor.fetchone()[0]
586
+
587
+ cursor.execute("SELECT COUNT(DISTINCT category) FROM text_items")
588
+ categories = cursor.fetchone()[0]
589
+
590
+ cursor.execute("SELECT COUNT(DISTINCT section) FROM text_items")
591
+ sections = cursor.fetchone()[0]
592
+
593
+ cursor.execute("SELECT category, COUNT(*) as count FROM text_items GROUP BY category ORDER BY count DESC LIMIT 10")
594
+ top_categories = cursor.fetchall()
595
+
596
+ cursor.execute("SELECT content_type, COUNT(*) as count FROM text_items GROUP BY content_type ORDER BY count DESC")
597
+ by_type = cursor.fetchall()
598
+
599
+ conn.close()
600
+
601
+ stats = f"""
602
+ ## 📊 Database Statistics
603
+
604
+ - **Total Items:** {total}
605
+ - **Total Categories:** {categories}
606
+ - **Total Sections:** {sections}
607
+
608
+ ---
609
+
610
+ ### 🏆 Top Categories by Item Count:
611
+ """
612
+ if top_categories:
613
+ for cat, count in top_categories:
614
+ stats += f"\n- **{cat}:** {count} items"
615
+ else:
616
+ stats += "\n*No data yet*"
617
+
618
+ stats += "\n\n### 📝 Items by Content Type:\n"
619
+ if by_type:
620
+ for ctype, count in by_type:
621
+ icon = CONTENT_TYPES.get(ctype, CONTENT_TYPES['other'])['icon']
622
+ label = CONTENT_TYPES.get(ctype, CONTENT_TYPES['other'])['label']
623
+ stats += f"\n- {icon} **{label}:** {count} items"
624
+ else:
625
+ stats += "\n*No data yet*"
626
+
627
+ return stats
628
+
629
+ def update_section_choices_on_category_change(category):
630
+ """Update section dropdown when category changes"""
631
+ sections = get_sections_by_category(category) if category else []
632
+ return gr.Dropdown(choices=sections)
633
+
634
+ def auto_filter(category, section, content_type):
635
+ """Automatically filter when any filter changes"""
636
+ return search_text_items(category, section, content_type)
637
+
638
+ def update_content_preview(items_df, evt: gr.SelectData):
639
+ """Update content preview when row is selected"""
640
+ if items_df is None or items_df.empty:
641
+ return ""
642
+
643
+ row_index = evt.index[0]
644
+ row = items_df.iloc[row_index]
645
+ item_id = row['id']
646
+
647
+ # Get full content from database
648
+ content = get_content_by_id(item_id)
649
+ return content
650
+
651
+ # Connect events for Add tab
652
+ new_category.change(
653
+ fn=update_section_choices_on_category_change,
654
+ inputs=[new_category],
655
+ outputs=[new_section]
656
+ )
657
+
658
+ add_btn.click(
659
+ fn=add_text_item,
660
+ inputs=[new_category, new_section, new_title, new_content, new_content_type],
661
+ outputs=[add_status, items_display, new_category, new_section]
662
+ ).then(
663
+ fn=lambda: ("", "", ""),
664
+ outputs=[new_title, new_content, new_section]
665
+ ).then(
666
+ fn=lambda: [
667
+ gr.Dropdown(choices=get_unique_categories()),
668
+ gr.Dropdown(choices=[""] + get_unique_categories()),
669
+ gr.Dropdown(choices=get_unique_categories()),
670
+ gr.Dropdown(choices=[""] + get_unique_categories()),
671
+ gr.Dropdown(choices=get_unique_categories()),
672
+ gr.Dropdown(choices=get_unique_categories()),
673
+ gr.Dropdown(choices=[""] + get_unique_categories()),
674
+ gr.Dropdown(choices=get_unique_categories())
675
+ ],
676
+ outputs=[
677
+ new_category, filter_category, edit_category,
678
+ rename_sec_category, rename_cat_old,
679
+ delete_cat_select, delete_sec_category, rename_sec_category
680
+ ]
681
+ )
682
+
683
+ # Auto-filter when any filter changes
684
+ filter_category.change(
685
+ fn=lambda cat: gr.Dropdown(choices=[""] + get_sections_by_category(cat)),
686
+ inputs=[filter_category],
687
+ outputs=[filter_section]
688
+ ).then(
689
+ fn=auto_filter,
690
+ inputs=[filter_category, filter_section, filter_type],
691
+ outputs=items_display
692
+ )
693
+
694
+ filter_section.change(
695
+ fn=auto_filter,
696
+ inputs=[filter_category, filter_section, filter_type],
697
+ outputs=items_display
698
+ )
699
+
700
+ filter_type.change(
701
+ fn=auto_filter,
702
+ inputs=[filter_category, filter_section, filter_type],
703
+ outputs=items_display
704
+ )
705
+
706
+ clear_filter_btn.click(
707
+ fn=clear_filters,
708
+ outputs=[filter_category, filter_section, filter_type, items_display]
709
+ )
710
+
711
+ # Load item for editing and preview when row is selected
712
+ items_display.select(
713
+ fn=load_item_for_editing,
714
+ inputs=[items_display],
715
+ outputs=[edit_id, edit_category, edit_section, edit_title, edit_content, edit_content_type]
716
+ )
717
+
718
+ items_display.select(
719
+ fn=update_content_preview,
720
+ inputs=[items_display],
721
+ outputs=[content_preview]
722
+ )
723
+
724
+ # Update section dropdown when edit category changes
725
+ edit_category.change(
726
+ fn=update_section_choices_on_category_change,
727
+ inputs=[edit_category],
728
+ outputs=[edit_section]
729
+ )
730
+
731
+ update_btn.click(
732
+ fn=update_text_item,
733
+ inputs=[edit_id, edit_category, edit_section, edit_title, edit_content, edit_content_type],
734
+ outputs=[edit_status, items_display, edit_category, edit_section]
735
+ ).then(
736
+ fn=lambda: [
737
+ gr.Dropdown(choices=get_unique_categories()),
738
+ gr.Dropdown(choices=[""] + get_unique_categories())
739
+ ],
740
+ outputs=[new_category, filter_category]
741
+ )
742
+
743
+ delete_btn.click(
744
+ fn=delete_text_item,
745
+ inputs=[edit_id],
746
+ outputs=[edit_status, items_display, edit_category, edit_section]
747
+ ).then(
748
+ fn=lambda: (None, "", "", "", "", "prompt"),
749
+ outputs=[edit_id, edit_category, edit_section, edit_title, edit_content, edit_content_type]
750
+ ).then(
751
+ fn=lambda: [
752
+ gr.Dropdown(choices=get_unique_categories()),
753
+ gr.Dropdown(choices=[""] + get_unique_categories())
754
+ ],
755
+ outputs=[new_category, filter_category]
756
+ )
757
+
758
+ # Rename category
759
+ rename_cat_btn.click(
760
+ fn=rename_category,
761
+ inputs=[rename_cat_old, rename_cat_new],
762
+ outputs=[rename_cat_status, items_display]
763
+ ).then(
764
+ fn=lambda: [
765
+ gr.Dropdown(choices=get_unique_categories()),
766
+ gr.Dropdown(choices=get_unique_categories()),
767
+ gr.Dropdown(choices=[""] + get_unique_categories()),
768
+ gr.Dropdown(choices=get_unique_categories()),
769
+ "", ""
770
+ ],
771
+ outputs=[rename_cat_old, new_category, filter_category, edit_category, rename_cat_old, rename_cat_new]
772
+ )
773
+
774
+ # Rename section
775
+ rename_sec_btn.click(
776
+ fn=rename_section,
777
+ inputs=[rename_sec_category, rename_sec_old, rename_sec_new],
778
+ outputs=[rename_sec_status, items_display]
779
+ ).then(
780
+ fn=lambda: ["", ""],
781
+ outputs=[rename_sec_old, rename_sec_new]
782
+ )
783
+
784
+ # Delete category
785
+ delete_cat_btn.click(
786
+ fn=delete_by_category,
787
+ inputs=[delete_cat_select, delete_cat_confirm],
788
+ outputs=[delete_cat_status, items_display]
789
+ ).then(
790
+ fn=lambda: [
791
+ gr.Dropdown(choices=get_unique_categories()),
792
+ gr.Dropdown(choices=get_unique_categories()),
793
+ gr.Dropdown(choices=[""] + get_unique_categories()),
794
+ False, ""
795
+ ],
796
+ outputs=[delete_cat_select, new_category, filter_category, delete_cat_confirm, delete_cat_select]
797
+ )
798
+
799
+ # Delete section
800
+ delete_sec_btn.click(
801
+ fn=delete_by_section,
802
+ inputs=[delete_sec_category, delete_sec_select, delete_sec_confirm],
803
+ outputs=[delete_sec_status, items_display]
804
+ ).then(
805
+ fn=lambda: [False, ""],
806
+ outputs=[delete_sec_confirm, delete_sec_select]
807
+ )
808
+
809
+ refresh_stats_btn.click(
810
+ fn=get_statistics,
811
+ outputs=stats_display
812
+ )
813
+
814
+ # Load initial statistics
815
+ app.load(fn=get_statistics, outputs=stats_display)
816
+
817
+ if __name__ == "__main__":
818
+ app.launch()
requirements.txt CHANGED
@@ -1 +1,2 @@
1
- flask==2.3.0
 
 
1
+ gradio>=4.44.0
2
+ pandas>=2.2.0
sync.ps1 CHANGED
@@ -37,7 +37,7 @@ if ($hfRemote -match 'huggingface\.co/spaces/([^/]+)/(.+?)(?:\.git)?$') {
37
  exit 1
38
  }
39
 
40
- Write-Host "`n=== 3-Way Sync: Local â†" GitHub â†" HF ===`n"
41
 
42
  function Get-RemoteCommit($remote) {
43
  $commit = git rev-parse "$remote/main" 2>$null
@@ -92,51 +92,48 @@ if (-not $isFirstPush) {
92
  Write-Host "Merging from GitHub..."
93
 
94
  if ($hasLocalChanges) {
95
- # Commit local changes first
96
  git commit -m "Local changes before merge" 2>$null | Out-Null
97
  }
98
 
99
  git merge github/main --no-edit 2>&1 | Out-Null
100
 
101
  if ($LASTEXITCODE -ne 0) {
102
- Write-Host "� Merge conflict with GitHub. Keeping local version..."
103
  git merge --abort 2>$null
104
- # Keep local but note we couldn't merge
105
  } else {
106
- Write-Host "âœ" Merged from GitHub"
107
  }
108
  }
109
 
110
  # Check if we need to merge HuggingFace
111
- $localCommit = git rev-parse HEAD 2>$null # Update after potential GitHub merge
112
  if ($hfExists -and $hfCommit -and $localCommit -ne $hfCommit) {
113
  Write-Host "Merging from HuggingFace..."
114
 
115
  git diff HEAD --quiet
116
  if ($LASTEXITCODE -ne 0) {
117
- # Commit any pending changes before merge
118
  git commit -m "Local changes before HF merge" 2>$null | Out-Null
119
  }
120
 
121
  git merge huggingface/main --no-edit 2>&1 | Out-Null
122
 
123
  if ($LASTEXITCODE -ne 0) {
124
- Write-Host "� Merge conflict with HuggingFace. Keeping local version..."
125
  git merge --abort 2>$null
126
  } else {
127
- Write-Host "âœ" Merged from HuggingFace"
128
  }
129
  }
130
 
131
  # Check if everything is in sync
132
  $localCommit = git rev-parse HEAD 2>$null
133
  if ($ghCommit -and $hfCommit -and $localCommit -eq $ghCommit -and $localCommit -eq $hfCommit) {
134
- Write-Host "âœ" All locations in sync"
135
  }
136
  }
137
 
138
  if ($PullOnly) {
139
- Write-Host "`nâœ" Pull complete`n"
140
  exit 0
141
  }
142
 
@@ -144,7 +141,7 @@ if ($PullOnly) {
144
  git diff HEAD --quiet
145
  if ($LASTEXITCODE -ne 0) {
146
  git commit -m $Message
147
- Write-Host "âœ" Committed: $Message"
148
  } else {
149
  # Check if we have commits to push
150
  $localCommit = git rev-parse HEAD 2>$null
@@ -154,18 +151,18 @@ if ($LASTEXITCODE -ne 0) {
154
  $ghCommit = Get-RemoteCommit "github"
155
  if ($localCommit -ne $ghCommit) { $needsPush = $true }
156
  } else {
157
- $needsPush = $true # First push
158
  }
159
 
160
  if ($hfExists) {
161
  $hfCommit = Get-RemoteCommit "huggingface"
162
  if ($localCommit -ne $hfCommit) { $needsPush = $true }
163
  } else {
164
- $needsPush = $true # First push
165
  }
166
 
167
  if (-not $needsPush) {
168
- Write-Host "âœ" Already up-to-date, nothing to push`n"
169
  exit 0
170
  }
171
  }
@@ -176,13 +173,13 @@ Write-Host "`nPushing to remotes..."
176
  # Push to GitHub
177
  git push github main 2>&1 | Out-Null
178
  if ($LASTEXITCODE -eq 0) {
179
- Write-Host "âœ" GitHub"
180
  } else {
181
  git push github main --force 2>&1 | Out-Null
182
  if ($LASTEXITCODE -eq 0) {
183
- Write-Host "âœ" GitHub (forced)"
184
  } else {
185
- Write-Host "✗ GitHub failed"
186
  }
187
  }
188
 
@@ -197,9 +194,9 @@ if ($Verbose) {
197
  }
198
 
199
  if ($LASTEXITCODE -eq 0) {
200
- Write-Host "âœ" Hugging Face: spaces/${hfUser}/${hfSpace}"
201
  } else {
202
- Write-Host "✗ Hugging Face FAILED!"
203
  Write-Host "`nTroubleshooting:"
204
  Write-Host "1. Does the space exist? https://huggingface.co/spaces/${hfUser}/${hfSpace}"
205
  Write-Host "2. Is your token valid? (HF Settings > Access Tokens)"
@@ -208,4 +205,4 @@ if ($LASTEXITCODE -eq 0) {
208
  exit 1
209
  }
210
 
211
- Write-Host "`nâœ" Sync complete`n"
 
37
  exit 1
38
  }
39
 
40
+ Write-Host "`n=== 3-Way Sync: Local <-> GitHub <-> HF ===`n"
41
 
42
  function Get-RemoteCommit($remote) {
43
  $commit = git rev-parse "$remote/main" 2>$null
 
92
  Write-Host "Merging from GitHub..."
93
 
94
  if ($hasLocalChanges) {
 
95
  git commit -m "Local changes before merge" 2>$null | Out-Null
96
  }
97
 
98
  git merge github/main --no-edit 2>&1 | Out-Null
99
 
100
  if ($LASTEXITCODE -ne 0) {
101
+ Write-Host "[!] Merge conflict with GitHub. Keeping local version..."
102
  git merge --abort 2>$null
 
103
  } else {
104
+ Write-Host "[OK] Merged from GitHub"
105
  }
106
  }
107
 
108
  # Check if we need to merge HuggingFace
109
+ $localCommit = git rev-parse HEAD 2>$null
110
  if ($hfExists -and $hfCommit -and $localCommit -ne $hfCommit) {
111
  Write-Host "Merging from HuggingFace..."
112
 
113
  git diff HEAD --quiet
114
  if ($LASTEXITCODE -ne 0) {
 
115
  git commit -m "Local changes before HF merge" 2>$null | Out-Null
116
  }
117
 
118
  git merge huggingface/main --no-edit 2>&1 | Out-Null
119
 
120
  if ($LASTEXITCODE -ne 0) {
121
+ Write-Host "[!] Merge conflict with HuggingFace. Keeping local version..."
122
  git merge --abort 2>$null
123
  } else {
124
+ Write-Host "[OK] Merged from HuggingFace"
125
  }
126
  }
127
 
128
  # Check if everything is in sync
129
  $localCommit = git rev-parse HEAD 2>$null
130
  if ($ghCommit -and $hfCommit -and $localCommit -eq $ghCommit -and $localCommit -eq $hfCommit) {
131
+ Write-Host "[OK] All locations in sync"
132
  }
133
  }
134
 
135
  if ($PullOnly) {
136
+ Write-Host "`n[OK] Pull complete`n"
137
  exit 0
138
  }
139
 
 
141
  git diff HEAD --quiet
142
  if ($LASTEXITCODE -ne 0) {
143
  git commit -m $Message
144
+ Write-Host "[OK] Committed: $Message"
145
  } else {
146
  # Check if we have commits to push
147
  $localCommit = git rev-parse HEAD 2>$null
 
151
  $ghCommit = Get-RemoteCommit "github"
152
  if ($localCommit -ne $ghCommit) { $needsPush = $true }
153
  } else {
154
+ $needsPush = $true
155
  }
156
 
157
  if ($hfExists) {
158
  $hfCommit = Get-RemoteCommit "huggingface"
159
  if ($localCommit -ne $hfCommit) { $needsPush = $true }
160
  } else {
161
+ $needsPush = $true
162
  }
163
 
164
  if (-not $needsPush) {
165
+ Write-Host "`n[OK] Already up-to-date, nothing to push`n"
166
  exit 0
167
  }
168
  }
 
173
  # Push to GitHub
174
  git push github main 2>&1 | Out-Null
175
  if ($LASTEXITCODE -eq 0) {
176
+ Write-Host "[OK] GitHub"
177
  } else {
178
  git push github main --force 2>&1 | Out-Null
179
  if ($LASTEXITCODE -eq 0) {
180
+ Write-Host "[OK] GitHub (forced)"
181
  } else {
182
+ Write-Host "[ERROR] GitHub failed"
183
  }
184
  }
185
 
 
194
  }
195
 
196
  if ($LASTEXITCODE -eq 0) {
197
+ Write-Host "[OK] Hugging Face: spaces/${hfUser}/${hfSpace}"
198
  } else {
199
+ Write-Host "[ERROR] Hugging Face FAILED!"
200
  Write-Host "`nTroubleshooting:"
201
  Write-Host "1. Does the space exist? https://huggingface.co/spaces/${hfUser}/${hfSpace}"
202
  Write-Host "2. Is your token valid? (HF Settings > Access Tokens)"
 
205
  exit 1
206
  }
207
 
208
+ Write-Host "`n[OK] Sync complete`n"