ginipick commited on
Commit
d23dac5
ยท
verified ยท
1 Parent(s): cabc3a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +788 -224
app.py CHANGED
@@ -1,260 +1,824 @@
1
- import subprocess # ๐Ÿฅฒ
2
- subprocess.run(
3
- "pip install flash-attn --no-build-isolation",
4
- env={"FLASH_ATTENTION_SKIP_CUDA_BUILD": "TRUE"},
5
- shell=True,
6
- )
7
 
8
- import spaces
9
- import gradio as gr
10
- import re
11
- import torch
12
- import os
13
- import json
14
- import time
15
- from pydantic import BaseModel
16
- from typing import Tuple
17
- from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
18
- from qwen_vl_utils import process_vision_info
19
- from PIL import Image
20
 
21
- os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
 
 
 
 
 
 
 
 
 
22
 
23
- # ----------------------- Model and Processor Loading ----------------------- #
24
- model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
25
- "Qwen/Qwen2.5-VL-7B-Instruct",
26
- torch_dtype=torch.bfloat16,
27
- attn_implementation="flash_attention_2",
28
- device_map="auto",
29
- )
30
- processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct")
31
 
32
- # ----------------------- Pydantic Model Definition ----------------------- #
33
- class GeneralRetrievalQuery(BaseModel):
34
- broad_topical_query: str
35
- broad_topical_explanation: str
36
- specific_detail_query: str
37
- specific_detail_explanation: str
38
- visual_element_query: str
39
- visual_element_explanation: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- def extract_json_with_regex(text):
42
- pattern = r'```(?:json)?\s*(.+?)\s*```'
43
- matches = re.findall(pattern, text, re.DOTALL)
44
- if matches:
45
- return matches[0]
46
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- def get_retrieval_prompt(prompt_name: str) -> Tuple[str, GeneralRetrievalQuery]:
49
- if prompt_name != "general":
50
- raise ValueError("Only 'general' prompt is available in this version")
51
- prompt = """You are an AI assistant specialized in document retrieval tasks. Given an image of a document page, your task is to generate retrieval queries that someone might use to find this document in a large corpus.
 
 
 
 
52
 
53
- Please generate 3 different types of retrieval queries:
 
 
 
 
 
 
 
54
 
55
- 1. A broad topical query: This should cover the main subject of the document.
56
- 2. A specific detail query: This should focus on a particular fact, figure, or point made in the document.
57
- 3. A visual element query: This should reference a chart, graph, image, or other visual component in the document, if present. Don't just reference the name of the visual element but generate a query which this illustration may help answer or be related to.
 
 
 
 
 
58
 
59
- Important guidelines:
60
- - Ensure the queries are relevant for retrieval tasks, not just describing the page content.
61
- - Frame the queries as if someone is searching for this document, not asking questions about its content.
62
- - Make the queries diverse and representative of different search strategies.
 
 
 
 
 
 
 
 
 
63
 
64
- For each query, also provide a brief explanation of why this query would be effective in retrieving this document.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- Format your response as a JSON object with the following structure:
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- {
69
- "broad_topical_query": "Your query here",
70
- "broad_topical_explanation": "Brief explanation",
71
- "specific_detail_query": "Your query here",
72
- "specific_detail_explanation": "Brief explanation",
73
- "visual_element_query": "Your query here",
74
- "visual_element_explanation": "Brief explanation"
75
- }
 
 
76
 
77
- If there are no relevant visual elements, replace the third query with another specific detail query.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- Here is the document image to analyze:
80
- <image>
 
 
 
 
 
 
 
81
 
82
- Generate the queries based on this image and provide the response in the specified JSON format."""
83
- return prompt, GeneralRetrievalQuery
84
 
85
- prompt, pydantic_model = get_retrieval_prompt("general")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- # ----------------------- Input Preprocessing ----------------------- #
88
- def _prep_data_for_input(image):
89
- messages = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  {
91
- "role": "user",
92
- "content": [
93
- {"type": "image", "image": image},
94
- {"type": "text", "text": prompt},
95
- ],
96
- }
97
  ]
98
- text = processor.apply_chat_template(
99
- messages, tokenize=False, add_generation_prompt=True
100
- )
101
- image_inputs, video_inputs = process_vision_info(messages)
102
- return processor(
103
- text=[text],
104
- images=image_inputs,
105
- videos=video_inputs,
106
- padding=True,
107
- return_tensors="pt",
108
- )
109
 
110
- # ----------------------- Output Formatting ----------------------- #
111
- def format_output(data: dict, output_format: str) -> str:
112
- """
113
- Convert the JSON data into the desired output format.
114
- output_format: "JSON", "Markdown", "Table"
115
- """
116
- if output_format == "JSON":
117
- # Wrap with code block for better display in Markdown view
118
- return f"```json\n{json.dumps(data, indent=2, ensure_ascii=False)}\n```"
119
- elif output_format == "Markdown":
120
- md_lines = []
121
- for key, value in data.items():
122
- md_lines.append(f"**{key.replace('_', ' ').title()}:** {value}")
123
- return "\n\n".join(md_lines)
124
- elif output_format == "Table":
125
- headers = ["Field", "Content"]
126
- separator = " | ".join(["---"] * len(headers))
127
- rows = [f"| {' | '.join(headers)} |", f"| {separator} |"]
128
- for key, value in data.items():
129
- rows.append(f"| {key.replace('_', ' ').title()} | {value} |")
130
- return "\n".join(rows)
131
- else:
132
- return f"```json\n{json.dumps(data, indent=2, ensure_ascii=False)}\n```"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- # ----------------------- Response Generation ----------------------- #
135
- @spaces.GPU
136
- def generate_response(image, output_format: str = "JSON"):
137
- inputs = _prep_data_for_input(image)
138
- inputs = inputs.to("cuda")
139
- generated_ids = model.generate(**inputs, max_new_tokens=200)
140
- generated_ids_trimmed = [
141
- out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
142
- ]
143
- output_text = processor.batch_decode(
144
- generated_ids_trimmed,
145
- skip_special_tokens=True,
146
- clean_up_tokenization_spaces=False,
147
- )[0]
148
 
 
 
 
149
  try:
150
- json_str = extract_json_with_regex(output_text)
151
- if json_str:
152
- parsed = json.loads(json_str)
153
- return format_output(parsed, output_format)
154
- parsed = json.loads(output_text)
155
- return format_output(parsed, output_format)
156
- except Exception:
157
- gr.Warning("Failed to parse JSON from output")
158
- return output_text
159
-
160
- # ----------------------- Interface Title and Description (in English) ----------------------- #
161
- title = "Elegant ColPali Query Generator using Qwen2.5-VL"
162
- description = """**ColPali** is a multimodal approach optimized for document retrieval.
163
- This interface uses the [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) model to generate relevant retrieval queries based on a document image.
164
-
165
- The queries include:
166
- - **Broad Topical Query:** Covers the main subject of the document.
167
- - **Specific Detail Query:** Focuses on a particular fact, figure, or point from the document.
168
- - **Visual Element Query:** References a visual component (e.g., chart, graph) from the document.
 
 
 
169
 
170
- Refer to the examples below to generate queries suitable for your document image.
171
- For more information, please see the associated blog post.
172
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- examples = [
175
- "examples/Approche_no_13_1977.pdf_page_22.jpg",
176
- "examples/SRCCL_Technical-Summary.pdf_page_7.jpg",
177
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- # ----------------------- Custom CSS ----------------------- #
180
- custom_css = """
181
- body {
182
- background: #f7f9fb;
183
- font-family: 'Segoe UI', sans-serif;
184
- color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
- header {
187
- text-align: center;
188
- padding: 20px;
189
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
190
  }
191
- header h1 {
192
- font-size: 3em;
193
- color: #2c3e50;
 
194
  }
195
- .gradio-container {
196
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }
198
- .gr-button {
199
- background-color: #3498db !important;
200
- color: #fff !important;
201
- border: none !important;
202
- padding: 10px 20px !important;
203
- border-radius: 5px !important;
204
- font-size: 1em !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
- .gr-button:hover {
207
- background-color: #2980b9 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  }
209
- .gr-gallery-item {
210
- border-radius: 10px;
211
- overflow: hidden;
212
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
- footer {
215
- text-align: center;
216
- padding: 20px 0;
217
- font-size: 0.9em;
218
- color: #555;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- # ----------------------- Gradio Interface ----------------------- #
223
- with gr.Blocks(css=custom_css, title=title) as demo:
224
- with gr.Column(variant="panel"):
225
- gr.Markdown(f"<header><h1>{title}</h1></header>")
226
- gr.Markdown(description)
227
-
228
- with gr.Tabs():
229
- with gr.TabItem("Query Generation"):
230
- gr.Markdown("### Generate Retrieval Queries from a Document Image")
231
- with gr.Row():
232
- image_input = gr.Image(label="Upload Document Image", type="pil")
233
- with gr.Row():
234
- output_format = gr.Radio(
235
- choices=["JSON", "Markdown", "Table"],
236
- value="JSON",
237
- label="Output Format",
238
- info="Select the desired output format."
239
- )
240
- generate_button = gr.Button("Generate Query")
241
- # ์ถœ๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ gr.Markdown์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ Markdown ๋ฐ Table ํ˜•์‹์ด ์ œ๋Œ€๋กœ ๋ Œ๋”๋ง๋˜๋„๋ก ํ•จ.
242
- output_text = gr.Markdown(label="Generated Query")
243
- with gr.Accordion("Examples", open=False):
244
- gr.Examples(
245
- label="Query Examples",
246
- examples=[
247
- "examples/Approche_no_13_1977.pdf_page_22.jpg",
248
- "examples/SRCCL_Technical-Summary.pdf_page_7.jpg",
249
- ],
250
- inputs=image_input,
251
- )
252
- generate_button.click(
253
- fn=generate_response,
254
- inputs=[image_input, output_format],
255
- outputs=output_text
256
- )
257
-
258
- gr.Markdown("<footer>Join our community on <a href='https://discord.gg/openfreeai' target='_blank'>Discord</a></footer>")
259
 
260
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import os, re, json, sqlite3
 
 
 
 
3
 
4
+ app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. CONFIGURATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
+ DB_FILE = "favorite_sites.json" # JSON file for backward compatibility
8
+ SQLITE_DB = "favorite_sites.db" # SQLite database for persistence
9
+
10
+ # Domains that commonly block iframes
11
+ BLOCKED_DOMAINS = [
12
+ "naver.com", "daum.net", "google.com",
13
+ "facebook.com", "instagram.com", "kakao.com",
14
+ "ycombinator.com"
15
+ ]
16
 
 
 
 
 
 
 
 
 
17
 
18
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. CURATED CATEGORIES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
19
+ CATEGORIES = {
20
+ "Productivity": [
21
+ "https://huggingface.co/spaces/openfree/Chart-GPT",
22
+ "https://huggingface.co/spaces/ginipick/AI-BOOK",
23
+ "https://huggingface.co/spaces/VIDraft/Voice-Clone-Podcast",
24
+ "https://huggingface.co/spaces/ginipick/PDF-EXAM",
25
+ "https://huggingface.co/spaces/ginigen/perflexity-clone",
26
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
27
+ "https://huggingface.co/spaces/ginipick/10m-marketing",
28
+
29
+ "https://huggingface.co/spaces/openfree/Live-Podcast",
30
+ "https://huggingface.co/spaces/openfree/AI-Podcast",
31
+ "https://huggingface.co/spaces/ginipick/QR-Canvas-plus",
32
+ "https://huggingface.co/spaces/openfree/Badge",
33
+ "https://huggingface.co/spaces/VIDraft/mouse-webgen",
34
+ "https://huggingface.co/spaces/openfree/Vibe-Game",
35
+ "https://huggingface.co/spaces/VIDraft/NH-Prediction",
36
+ "https://huggingface.co/spaces/ginipick/NH-Korea",
37
+ "https://huggingface.co/spaces/openfree/Naming",
38
+ "https://huggingface.co/spaces/ginipick/Change-Hair",
39
+ ],
40
+ "Multimodal": [
41
+ "https://huggingface.co/spaces/Heartsync/adult",
42
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
43
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video2",
44
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-video",
45
+ "https://huggingface.co/spaces/Heartsync/WAN-VIDEO-AUDIO",
46
+ "https://huggingface.co/spaces/Heartsync/wan2-1-fast-security",
47
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
48
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA-V1",
49
+ "https://huggingface.co/spaces/ginigen/Flux-VIDEO",
50
+ "https://huggingface.co/spaces/openfree/Multilingual-TTS",
51
+ "https://huggingface.co/spaces/VIDraft/ACE-Singer",
52
+ "https://huggingface.co/spaces/openfree/DreamO-video",
53
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
54
+ "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
55
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
56
+ "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
57
+ "https://huggingface.co/spaces/fantaxy/Remove-Video-Background",
58
+ ],
59
+ "Professional": [
60
+ "https://huggingface.co/spaces/Heartsync/Novel-NSFW",
61
+ "https://huggingface.co/spaces/fantaxy/fantasy-novel",
62
+ "https://huggingface.co/spaces/VIDraft/money-radar",
63
+ "https://huggingface.co/spaces/immunobiotech/drug-discovery",
64
+ "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
65
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
66
+ "https://huggingface.co/spaces/VIDraft/Fashion-Fit",
67
+ "https://huggingface.co/spaces/openfree/Stock-Trading-Analysis",
68
+ "https://huggingface.co/spaces/ginipick/AgentX-Papers",
69
+ "https://huggingface.co/spaces/Heartsync/Papers-Leaderboard",
70
+ "https://huggingface.co/spaces/VIDraft/PapersImpact",
71
+ "https://huggingface.co/spaces/ginigen/multimodal-chat-mbti-korea",
72
+ ],
73
+ "Image": [
74
+ "https://huggingface.co/spaces/ginigen/FLUX-Ghibli-LoRA2",
75
+ "https://huggingface.co/spaces/aiqcamp/REMOVAL-TEXT-IMAGE",
76
+ "https://huggingface.co/spaces/VIDraft/BAGEL-Websearch",
77
+ "https://huggingface.co/spaces/ginigen/Every-Text",
78
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
79
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
80
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
81
+ "https://huggingface.co/spaces/ginigen/canvas-studio",
82
+ "https://huggingface.co/spaces/VIDraft/ReSize-Image-Outpainting",
83
+ "https://huggingface.co/spaces/Heartsync/FLUX-Vision",
84
+ "https://huggingface.co/spaces/fantos/textcutobject",
85
+ "https://huggingface.co/spaces/aiqtech/imaginpaint",
86
+ "https://huggingface.co/spaces/openfree/ColorRevive",
87
+ "https://huggingface.co/spaces/openfree/ultpixgen",
88
+ "https://huggingface.co/spaces/VIDraft/Polaroid-Style",
89
+ "https://huggingface.co/spaces/ginigen/VisualCloze",
90
+ "https://huggingface.co/spaces/fantaxy/ofai-flx-logo",
91
+ "https://huggingface.co/spaces/ginigen/interior-design",
92
+ "https://huggingface.co/spaces/ginigen/MagicFace-V3",
93
+ "https://huggingface.co/spaces/fantaxy/flx-pulid",
94
+ "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering",
95
+ "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
96
+ "https://huggingface.co/spaces/VIDraft/stable-diffusion-3.5-large-turboX",
97
+ "https://huggingface.co/spaces/aiqtech/flxgif",
98
+ "https://huggingface.co/spaces/openfree/VectorFlow",
99
+
100
+ # "https://huggingface.co/spaces/ginigen/3D-LLAMA",
101
+ # "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
102
+
103
+ ],
104
+ "LLM / VLM": [
105
+ "https://huggingface.co/spaces/ginigen/deepseek-r1-0528-API",
106
+ "https://huggingface.co/spaces/aiqcamp/Mistral-Devstral-API"
107
+ "https://huggingface.co/spaces/aiqcamp/deepseek-r1-0528",
108
+ "https://huggingface.co/spaces/aiqcamp/deepseek-r1-0528-qwen3-8b",
109
+ "https://huggingface.co/spaces/aiqcamp/deepseek-r1-0528",
110
+ "https://huggingface.co/spaces/aiqcamp/Mistral-Devstral-API",
111
+ "https://huggingface.co/spaces/VIDraft/Mistral-RAG-BitSix",
112
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
113
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
114
+ "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
115
+ "https://huggingface.co/spaces/aiqcamp/gemini-2.5-flash-preview",
116
+ "https://huggingface.co/spaces/openfree/qwen3-30b-a3b-research",
117
+ "https://huggingface.co/spaces/openfree/qwen3-235b-a22b-research",
118
+ "https://huggingface.co/spaces/openfree/Llama-4-Maverick-17B-Research",
119
+ ],
120
+ }
121
 
122
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. DATABASE FUNCTIONS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
+ def init_db():
124
+ # Initialize JSON file if it doesn't exist
125
+ if not os.path.exists(DB_FILE):
126
+ with open(DB_FILE, "w", encoding="utf-8") as f:
127
+ json.dump([], f, ensure_ascii=False)
128
+
129
+ # Initialize SQLite database
130
+ conn = sqlite3.connect(SQLITE_DB)
131
+ cursor = conn.cursor()
132
+ cursor.execute('''
133
+ CREATE TABLE IF NOT EXISTS urls (
134
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
135
+ url TEXT UNIQUE NOT NULL,
136
+ date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP
137
+ )
138
+ ''')
139
+ conn.commit()
140
+
141
+ # If we have data in JSON but not in SQLite (first run with new SQLite DB),
142
+ # migrate the data from JSON to SQLite
143
+ json_urls = load_json()
144
+ if json_urls:
145
+ db_urls = load_db_sqlite()
146
+ for url in json_urls:
147
+ if url not in db_urls:
148
+ add_url_to_sqlite(url)
149
+
150
+ conn.close()
151
 
152
+ def load_json():
153
+ """Load URLs from JSON file (for backward compatibility)"""
154
+ try:
155
+ with open(DB_FILE, "r", encoding="utf-8") as f:
156
+ raw = json.load(f)
157
+ return raw if isinstance(raw, list) else []
158
+ except Exception:
159
+ return []
160
 
161
+ def save_json(lst):
162
+ """Save URLs to JSON file (for backward compatibility)"""
163
+ try:
164
+ with open(DB_FILE, "w", encoding="utf-8") as f:
165
+ json.dump(lst, f, ensure_ascii=False, indent=2)
166
+ return True
167
+ except Exception:
168
+ return False
169
 
170
+ def load_db_sqlite():
171
+ """Load URLs from SQLite database"""
172
+ conn = sqlite3.connect(SQLITE_DB)
173
+ cursor = conn.cursor()
174
+ cursor.execute("SELECT url FROM urls ORDER BY date_added DESC")
175
+ urls = [row[0] for row in cursor.fetchall()]
176
+ conn.close()
177
+ return urls
178
 
179
+ def add_url_to_sqlite(url):
180
+ """Add a URL to SQLite database"""
181
+ conn = sqlite3.connect(SQLITE_DB)
182
+ cursor = conn.cursor()
183
+ try:
184
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
185
+ conn.commit()
186
+ success = True
187
+ except sqlite3.IntegrityError:
188
+ # URL already exists
189
+ success = False
190
+ conn.close()
191
+ return success
192
 
193
+ def update_url_in_sqlite(old_url, new_url):
194
+ """Update a URL in SQLite database"""
195
+ conn = sqlite3.connect(SQLITE_DB)
196
+ cursor = conn.cursor()
197
+ try:
198
+ cursor.execute("UPDATE urls SET url = ? WHERE url = ?", (new_url, old_url))
199
+ if cursor.rowcount > 0:
200
+ conn.commit()
201
+ success = True
202
+ else:
203
+ success = False
204
+ except sqlite3.IntegrityError:
205
+ # New URL already exists
206
+ success = False
207
+ conn.close()
208
+ return success
209
 
210
+ def delete_url_from_sqlite(url):
211
+ """Delete a URL from SQLite database"""
212
+ conn = sqlite3.connect(SQLITE_DB)
213
+ cursor = conn.cursor()
214
+ cursor.execute("DELETE FROM urls WHERE url = ?", (url,))
215
+ if cursor.rowcount > 0:
216
+ conn.commit()
217
+ success = True
218
+ else:
219
+ success = False
220
+ conn.close()
221
+ return success
222
 
223
+ def load_db():
224
+ """Primary function to load URLs - prioritizes SQLite DB but falls back to JSON"""
225
+ urls = load_db_sqlite()
226
+ if not urls:
227
+ # If SQLite DB is empty, try loading from JSON
228
+ urls = load_json()
229
+ # If we found URLs in JSON, migrate them to SQLite
230
+ for url in urls:
231
+ add_url_to_sqlite(url)
232
+ return urls
233
 
234
+ def save_db(lst):
235
+ """Save URLs to both SQLite and JSON"""
236
+ # Get existing URLs from SQLite for comparison
237
+ existing_urls = load_db_sqlite()
238
+
239
+ # Clear all URLs from SQLite and add the new list
240
+ conn = sqlite3.connect(SQLITE_DB)
241
+ cursor = conn.cursor()
242
+ cursor.execute("DELETE FROM urls")
243
+ for url in lst:
244
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
245
+ conn.commit()
246
+ conn.close()
247
+
248
+ # Also save to JSON for backward compatibility
249
+ return save_json(lst)
250
 
251
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. URL HELPERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
252
+ def direct_url(hf_url):
253
+ m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
254
+ if not m:
255
+ return hf_url
256
+ owner, name = m.groups()
257
+ owner = owner.lower()
258
+ name = name.replace('.', '-').replace('_', '-').lower()
259
+ return f"https://{owner}-{name}.hf.space"
260
 
261
+ def screenshot_url(url):
262
+ return f"https://image.thum.io/get/fullpage/{url}"
263
 
264
+ def process_url_for_preview(url):
265
+ """Returns (preview_url, mode)"""
266
+ # Handle blocked domains first
267
+ if any(d for d in BLOCKED_DOMAINS if d in url):
268
+ return screenshot_url(url), "snapshot"
269
+
270
+ # Special case handling for problematic URLs
271
+ if "vibe-coding-tetris" in url or "World-of-Tank-GAME" in url or "Minesweeper-Game" in url:
272
+ return screenshot_url(url), "snapshot"
273
+
274
+ # General HF space handling
275
+ try:
276
+ if "huggingface.co/spaces" in url:
277
+ parts = url.rstrip("/").split("/")
278
+ if len(parts) >= 5:
279
+ owner = parts[-2]
280
+ name = parts[-1]
281
+ embed_url = f"https://huggingface.co/spaces/{owner}/{name}/embed"
282
+ return embed_url, "iframe"
283
+ except Exception:
284
+ return screenshot_url(url), "snapshot"
285
+
286
+ # Default handling
287
+ return url, "iframe"
288
 
289
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. API ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
290
+ @app.route('/api/category')
291
+ def api_category():
292
+ cat = request.args.get('name', '')
293
+ urls = CATEGORIES.get(cat, [])
294
+
295
+ # Add pagination for categories as well
296
+ page = int(request.args.get('page', 1))
297
+ per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
298
+
299
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
300
+ start = (page - 1) * per_page
301
+ end = min(start + per_page, len(urls))
302
+
303
+ urls_page = urls[start:end]
304
+
305
+ items = [
306
  {
307
+ "title": url.split('/')[-1],
308
+ "owner": url.split('/')[-2] if '/spaces/' in url else '',
309
+ "iframe": direct_url(url),
310
+ "shot": screenshot_url(url),
311
+ "hf": url
312
+ } for url in urls_page
313
  ]
314
+
315
+ return jsonify({
316
+ "items": items,
317
+ "page": page,
318
+ "total_pages": total_pages
319
+ })
 
 
 
 
 
320
 
321
+ @app.route('/api/favorites')
322
+ def api_favorites():
323
+ # Load URLs from SQLite database
324
+ urls = load_db()
325
+
326
+ page = int(request.args.get('page', 1))
327
+ per_page = int(request.args.get('per_page', 4)) # Changed to 4 per page
328
+
329
+ total_pages = max(1, (len(urls) + per_page - 1) // per_page)
330
+ start = (page - 1) * per_page
331
+ end = min(start + per_page, len(urls))
332
+
333
+ urls_page = urls[start:end]
334
+
335
+ result = []
336
+ for url in urls_page:
337
+ try:
338
+ preview_url, mode = process_url_for_preview(url)
339
+ result.append({
340
+ "title": url.split('/')[-1],
341
+ "url": url,
342
+ "preview_url": preview_url,
343
+ "mode": mode
344
+ })
345
+ except Exception:
346
+ # Fallback to screenshot mode
347
+ result.append({
348
+ "title": url.split('/')[-1],
349
+ "url": url,
350
+ "preview_url": screenshot_url(url),
351
+ "mode": "snapshot"
352
+ })
353
+
354
+ return jsonify({
355
+ "items": result,
356
+ "page": page,
357
+ "total_pages": total_pages
358
+ })
359
 
360
+ @app.route('/api/url/add', methods=['POST'])
361
+ def add_url():
362
+ url = request.form.get('url', '').strip()
363
+ if not url:
364
+ return jsonify({"success": False, "message": "URL is required"})
 
 
 
 
 
 
 
 
 
365
 
366
+ # SQLite์— ์ถ”๊ฐ€ ์‹œ๋„
367
+ conn = sqlite3.connect(SQLITE_DB)
368
+ cursor = conn.cursor()
369
  try:
370
+ cursor.execute("INSERT INTO urls (url) VALUES (?)", (url,))
371
+ conn.commit()
372
+ success = True
373
+ except sqlite3.IntegrityError:
374
+ # URL์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ
375
+ success = False
376
+ except Exception as e:
377
+ print(f"SQLite error: {str(e)}")
378
+ success = False
379
+ finally:
380
+ conn.close()
381
+
382
+ if not success:
383
+ return jsonify({"success": False, "message": "URL already exists or could not be added"})
384
+
385
+ # JSON ํŒŒ์ผ์—๋„ ์ถ”๊ฐ€ (๋ฐฑ์—…์šฉ)
386
+ data = load_json()
387
+ if url not in data:
388
+ data.insert(0, url)
389
+ save_json(data)
390
+
391
+ return jsonify({"success": True, "message": "URL added successfully"})
392
 
393
+ @app.route('/api/url/update', methods=['POST'])
394
+ def update_url():
395
+ old = request.form.get('old', '')
396
+ new = request.form.get('new', '').strip()
397
+
398
+ if not new:
399
+ return jsonify({"success": False, "message": "New URL is required"})
400
+
401
+ # Update in SQLite DB
402
+ if not update_url_in_sqlite(old, new):
403
+ return jsonify({"success": False, "message": "URL not found or new URL already exists"})
404
+
405
+ # Also update JSON file for backward compatibility
406
+ data = load_json()
407
+ try:
408
+ idx = data.index(old)
409
+ data[idx] = new
410
+ save_json(data)
411
+ except ValueError:
412
+ # If URL not in JSON, add it
413
+ data.append(new)
414
+ save_json(data)
415
+
416
+ return jsonify({"success": True, "message": "URL updated successfully"})
417
 
418
+ @app.route('/api/url/delete', methods=['POST'])
419
+ def delete_url():
420
+ url = request.form.get('url', '')
421
+
422
+ # Delete from SQLite DB
423
+ if not delete_url_from_sqlite(url):
424
+ return jsonify({"success": False, "message": "URL not found"})
425
+
426
+ # Also update JSON file for backward compatibility
427
+ data = load_json()
428
+ try:
429
+ data.remove(url)
430
+ save_json(data)
431
+ except ValueError:
432
+ pass
433
+
434
+ return jsonify({"success": True, "message": "URL deleted successfully"})
435
 
436
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. MAIN ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
437
+ @app.route('/')
438
+ def home():
439
+ os.makedirs('templates', exist_ok=True)
440
+
441
+ with open('templates/index.html', 'w', encoding='utf-8') as fp:
442
+ fp.write(r'''<!DOCTYPE html>
443
+ <html>
444
+ <head>
445
+ <meta charset="utf-8">
446
+ <meta name="viewport" content="width=device-width, initial-scale=1">
447
+ <title>Web Gallery</title>
448
+ <style>
449
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;600&display=swap');
450
+ body{margin:0;font-family:Nunito,sans-serif;background:#f6f8fb;}
451
+ .tabs{display:flex;flex-wrap:wrap;gap:8px;padding:16px;}
452
+ .tab{padding:6px 14px;border:none;border-radius:18px;background:#e2e8f0;font-weight:600;cursor:pointer;}
453
+ .tab.active{background:#a78bfa;color:#1a202c;}
454
+ .tab.manage{background:#ff6e91;color:white;}
455
+ .tab.manage.active{background:#ff2d62;color:white;}
456
+ /* Updated grid to show 2x2 layout */
457
+ .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;padding:0 16px 60px;max-width:1200px;margin:0 auto;}
458
+ @media(max-width:800px){.grid{grid-template-columns:1fr;}}
459
+ /* Increased card height for larger display */
460
+ .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);overflow:hidden;height:540px;display:flex;flex-direction:column;position:relative;}
461
+ .frame{flex:1;position:relative;overflow:hidden;}
462
+ .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0;}
463
+ .frame img{width:100%;height:100%;object-fit:cover;}
464
+ .card-label{position:absolute;top:10px;left:10px;padding:4px 8px;border-radius:4px;font-size:11px;font-weight:bold;z-index:100;text-transform:uppercase;letter-spacing:0.5px;box-shadow:0 2px 4px rgba(0,0,0,0.2);}
465
+ .label-live{background:linear-gradient(135deg, #00c6ff, #0072ff);color:white;}
466
+ .label-static{background:linear-gradient(135deg, #ff9a9e, #fad0c4);color:#333;}
467
+ .foot{height:44px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee;}
468
+ .foot a{font-size:.82rem;font-weight:700;color:#4a6dd8;text-decoration:none;}
469
+ .pagination{display:flex;justify-content:center;margin:20px 0;gap:10px;}
470
+ .pagination button{padding:5px 15px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;}
471
+ .pagination button:disabled{opacity:0.5;cursor:not-allowed;}
472
+ .manage-panel{background:white;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.08);margin:16px;padding:20px;}
473
+ .form-group{margin-bottom:15px;}
474
+ .form-group label{display:block;margin-bottom:5px;font-weight:600;}
475
+ .form-control{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box;}
476
+ .btn{padding:8px 15px;border:none;border-radius:4px;cursor:pointer;font-weight:600;}
477
+ .btn-primary{background:#4a6dd8;color:white;}
478
+ .btn-danger{background:#e53e3e;color:white;}
479
+ .btn-success{background:#38a169;color:white;}
480
+ .status{padding:10px;margin:10px 0;border-radius:4px;display:none;}
481
+ .status.success{display:block;background:#c6f6d5;color:#22543d;}
482
+ .status.error{display:block;background:#fed7d7;color:#822727;}
483
+ .url-list{margin:20px 0;border:1px solid #eee;border-radius:4px;max-height:300px;overflow-y:auto;}
484
+ .url-item{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;}
485
+ .url-item:last-child{border-bottom:none;}
486
+ .url-controls{display:flex;gap:5px;}
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <header style="text-align: center; padding: 20px; background: linear-gradient(135deg, #f6f8fb, #e2e8f0); border-bottom: 1px solid #ddd;">
491
+ <h1 style="margin-bottom: 10px;">๐ŸŒŸAI Playground</h1>
492
+ <p>
493
+ <a href="https://discord.gg/openfreeai" target="_blank"><img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge"></a>
494
+ </p>
495
+ </header>
496
+ <div class="tabs" id="tabs"></div>
497
+ <div id="content"></div>
498
+ <script>
499
+ // Basic configuration
500
+ const cats = {{cats|tojson}};
501
+ const tabs = document.getElementById('tabs');
502
+ const content = document.getElementById('content');
503
+ let active = "";
504
+ let currentPage = 1;
505
+ // Simple utility functions
506
+ function loadHTML(url, callback) {
507
+ const xhr = new XMLHttpRequest();
508
+ xhr.open('GET', url, true);
509
+ xhr.onreadystatechange = function() {
510
+ if (xhr.readyState === 4 && xhr.status === 200) {
511
+ callback(xhr.responseText);
512
+ }
513
+ };
514
+ xhr.send();
515
  }
516
+ function makeRequest(url, method, data, callback) {
517
+ const xhr = new XMLHttpRequest();
518
+ xhr.open(method, url, true);
519
+ xhr.onreadystatechange = function() {
520
+ if (xhr.readyState === 4 && xhr.status === 200) {
521
+ callback(JSON.parse(xhr.responseText));
522
+ }
523
+ };
524
+ if (method === 'POST') {
525
+ xhr.send(data);
526
+ } else {
527
+ xhr.send();
528
+ }
529
  }
530
+ function updateTabs() {
531
+ Array.from(tabs.children).forEach(b => {
532
+ b.classList.toggle('active', b.dataset.c === active);
533
+ });
534
  }
535
+ // Tab handlers
536
+ function loadCategory(cat, page) {
537
+ if(cat === active && currentPage === page) return;
538
+ active = cat;
539
+ currentPage = page || 1;
540
+ updateTabs();
541
+
542
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
543
+
544
+ makeRequest('/api/category?name=' + encodeURIComponent(cat) + '&page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
545
+ let html = '<div class="grid">';
546
+
547
+ if(data.items.length === 0) {
548
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No items in this category.</p>';
549
+ } else {
550
+ data.items.forEach(item => {
551
+ html += `
552
+ <div class="card">
553
+ <div class="card-label label-live">LIVE</div>
554
+ <div class="frame">
555
+ <iframe src="${item.iframe}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
556
+ </div>
557
+ <div class="foot">
558
+ <a href="${item.hf}" target="_blank">${item.title}</a>
559
+ </div>
560
+ </div>
561
+ `;
562
+ });
563
+ }
564
+
565
+ html += '</div>';
566
+
567
+ // Add pagination
568
+ html += `
569
+ <div class="pagination">
570
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage-1})">ยซ Previous</button>
571
+ <span>Page ${currentPage} of ${data.total_pages}</span>
572
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadCategory('${cat}', ${currentPage+1})">Next ยป</button>
573
+ </div>
574
+ `;
575
+
576
+ content.innerHTML = html;
577
+ });
578
  }
579
+ function loadFavorites(page) {
580
+ if(active === 'Favorites' && currentPage === page) return;
581
+ active = 'Favorites';
582
+ currentPage = page || 1;
583
+ updateTabs();
584
+
585
+ content.innerHTML = '<p style="text-align:center;padding:40px">Loadingโ€ฆ</p>';
586
+
587
+ makeRequest('/api/favorites?page=' + currentPage + '&per_page=4', 'GET', null, function(data) {
588
+ let html = '<div class="grid">';
589
+
590
+ if(data.items.length === 0) {
591
+ html += '<p style="grid-column:1/-1;text-align:center;padding:40px">No favorites saved yet.</p>';
592
+ } else {
593
+ data.items.forEach(item => {
594
+ if(item.mode === 'snapshot') {
595
+ html += `
596
+ <div class="card">
597
+ <div class="card-label label-static">Static</div>
598
+ <div class="frame">
599
+ <img src="${item.preview_url}" loading="lazy">
600
+ </div>
601
+ <div class="foot">
602
+ <a href="${item.url}" target="_blank">${item.title}</a>
603
+ </div>
604
+ </div>
605
+ `;
606
+ } else {
607
+ html += `
608
+ <div class="card">
609
+ <div class="card-label label-live">LIVE</div>
610
+ <div class="frame">
611
+ <iframe src="${item.preview_url}" loading="lazy" sandbox="allow-forms allow-modals allow-popups allow-same-origin allow-scripts allow-downloads"></iframe>
612
+ </div>
613
+ <div class="foot">
614
+ <a href="${item.url}" target="_blank">${item.title}</a>
615
+ </div>
616
+ </div>
617
+ `;
618
+ }
619
+ });
620
+ }
621
+
622
+ html += '</div>';
623
+
624
+ // Add pagination
625
+ html += `
626
+ <div class="pagination">
627
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="loadFavorites(${currentPage-1})">ยซ Previous</button>
628
+ <span>Page ${currentPage} of ${data.total_pages}</span>
629
+ <button ${currentPage >= data.total_pages ? 'disabled' : ''} onclick="loadFavorites(${currentPage+1})">Next ยป</button>
630
+ </div>
631
+ `;
632
+
633
+ content.innerHTML = html;
634
+ });
635
+ }
636
+ function loadManage() {
637
+ if(active === 'Manage') return;
638
+ active = 'Manage';
639
+ updateTabs();
640
+
641
+ content.innerHTML = `
642
+ <div class="manage-panel">
643
+ <h2>Add New URL</h2>
644
+ <div class="form-group">
645
+ <label for="new-url">URL</label>
646
+ <input type="text" id="new-url" class="form-control" placeholder="https://example.com">
647
+ </div>
648
+ <button onclick="addUrl()" class="btn btn-primary">Add URL</button>
649
+ <div id="add-status" class="status"></div>
650
+
651
+ <h2>Manage Saved URLs</h2>
652
+ <div id="url-list" class="url-list">Loading...</div>
653
+ </div>
654
+ `;
655
+
656
+ loadUrlList();
657
+ }
658
+ // URL management functions
659
+ function loadUrlList() {
660
+ makeRequest('/api/favorites?per_page=100', 'GET', null, function(data) {
661
+ const urlList = document.getElementById('url-list');
662
+
663
+ if(data.items.length === 0) {
664
+ urlList.innerHTML = '<p style="text-align:center;padding:20px">No URLs saved yet.</p>';
665
+ return;
666
+ }
667
+
668
+ let html = '';
669
+ data.items.forEach(item => {
670
+ // Escape the URL to prevent JavaScript injection when used in onclick handlers
671
+ const escapedUrl = item.url.replace(/'/g, "\\'");
672
+
673
+ html += `
674
+ <div class="url-item">
675
+ <div>${item.url}</div>
676
+ <div class="url-controls">
677
+ <button class="btn" onclick="editUrl('${escapedUrl}')">Edit</button>
678
+ <button class="btn btn-danger" onclick="deleteUrl('${escapedUrl}')">Delete</button>
679
+ </div>
680
+ </div>
681
+ `;
682
+ });
683
+
684
+ urlList.innerHTML = html;
685
+ });
686
  }
687
+ function addUrl() {
688
+ const url = document.getElementById('new-url').value.trim();
689
+
690
+ if(!url) {
691
+ showStatus('add-status', 'Please enter a URL', false);
692
+ return;
693
+ }
694
+
695
+ const formData = new FormData();
696
+ formData.append('url', url);
697
+
698
+ makeRequest('/api/url/add', 'POST', formData, function(data) {
699
+ showStatus('add-status', data.message, data.success);
700
+ if(data.success) {
701
+ document.getElementById('new-url').value = '';
702
+ loadUrlList();
703
+ // If currently in Favorites tab, reload to see changes immediately
704
+ if(active === 'Favorites') {
705
+ loadFavorites(currentPage);
706
+ }
707
+ }
708
+ });
709
  }
710
+ function editUrl(url) {
711
+ // Decode URL if it was previously escaped
712
+ const decodedUrl = url.replace(/\\'/g, "'");
713
+ const newUrl = prompt('Edit URL:', decodedUrl);
714
+
715
+ if(!newUrl || newUrl === decodedUrl) return;
716
+
717
+ const formData = new FormData();
718
+ formData.append('old', decodedUrl);
719
+ formData.append('new', newUrl);
720
+
721
+ makeRequest('/api/url/update', 'POST', formData, function(data) {
722
+ if(data.success) {
723
+ loadUrlList();
724
+ // If currently in Favorites tab, reload to see changes immediately
725
+ if(active === 'Favorites') {
726
+ loadFavorites(currentPage);
727
+ }
728
+ } else {
729
+ alert(data.message);
730
+ }
731
+ });
732
  }
733
+ function deleteUrl(url) {
734
+ // Decode URL if it was previously escaped
735
+ const decodedUrl = url.replace(/\\'/g, "'");
736
+ if(!confirm('Are you sure you want to delete this URL?')) return;
737
+
738
+ const formData = new FormData();
739
+ formData.append('url', decodedUrl);
740
+
741
+ makeRequest('/api/url/delete', 'POST', formData, function(data) {
742
+ if(data.success) {
743
+ loadUrlList();
744
+ // If currently in Favorites tab, reload to see changes immediately
745
+ if(active === 'Favorites') {
746
+ loadFavorites(currentPage);
747
+ }
748
+ } else {
749
+ alert(data.message);
750
+ }
751
+ });
752
  }
753
+ function showStatus(id, message, success) {
754
+ const status = document.getElementById(id);
755
+ status.textContent = message;
756
+ status.className = success ? 'status success' : 'status error';
757
+ setTimeout(() => {
758
+ status.className = 'status';
759
+ }, 3000);
760
+ }
761
+ // Create tabs
762
+ // Favorites tab first
763
+ const favTab = document.createElement('button');
764
+ favTab.className = 'tab';
765
+ favTab.textContent = 'Favorites';
766
+ favTab.dataset.c = 'Favorites';
767
+ favTab.onclick = function() { loadFavorites(1); };
768
+ tabs.appendChild(favTab);
769
+ // Category tabs
770
+ cats.forEach(c => {
771
+ const b = document.createElement('button');
772
+ b.className = 'tab';
773
+ b.textContent = c;
774
+ b.dataset.c = c;
775
+ b.onclick = function() { loadCategory(c, 1); };
776
+ tabs.appendChild(b);
777
+ });
778
+ // Manage tab last
779
+ const manageTab = document.createElement('button');
780
+ manageTab.className = 'tab manage';
781
+ manageTab.textContent = 'Manage';
782
+ manageTab.dataset.c = 'Manage';
783
+ manageTab.onclick = function() { loadManage(); };
784
+ tabs.appendChild(manageTab);
785
+ // Start with Favorites tab
786
+ loadFavorites(1);
787
+ </script>
788
+ </body>
789
+ </html>''')
790
+
791
+ # Return the rendered template
792
+ return render_template('index.html', cats=list(CATEGORIES.keys()))
793
 
794
+ # Initialize database on startup
795
+ init_db()
796
+
797
+ # Define a function to ensure database consistency
798
+ def ensure_db_consistency():
799
+ # Make sure we have the latest data in both JSON and SQLite
800
+ urls = load_db_sqlite()
801
+ save_json(urls)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
+ # For Flask 2.0+ compatibility
804
+ @app.before_request
805
+ def before_request_func():
806
+ # Use a flag to run this only once
807
+ if not hasattr(app, '_got_first_request'):
808
+ ensure_db_consistency()
809
+ app._got_first_request = True
810
+
811
+ if __name__ == '__main__':
812
+ # ์•ฑ ์‹œ์ž‘ ์ „์— ๋ช…์‹œ์ ์œผ๋กœ DB ์ดˆ๊ธฐํ™”
813
+ print("Initializing database...")
814
+ init_db()
815
+
816
+ # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฐ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
817
+ db_path = os.path.abspath(SQLITE_DB)
818
+ print(f"SQLite DB path: {db_path}")
819
+ if os.path.exists(SQLITE_DB):
820
+ print(f"Database file exists, size: {os.path.getsize(SQLITE_DB)} bytes")
821
+ else:
822
+ print("Warning: Database file does not exist after initialization!")
823
+
824
+ app.run(host='0.0.0.0', port=7860)