milwright commited on
Commit
7bdbd56
Β·
0 Parent(s):

Improve configuration interface and deployment package

Browse files

- Move configuration status below export button in deployment
- Add 57-character limit to space description field
- Fix syntax error in SPACE_TEMPLATE
- Update API key and access code field labels
- Remove grey backgrounds from configuration sections
- Add clickable example prompt buttons in preview
- Enhance configuration status with URL grounding details

.gitattributes ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ img/img1.png filter=lfs diff=lfs merge=lfs -text
37
+ img/img10.png filter=lfs diff=lfs merge=lfs -text
38
+ img/img11.png filter=lfs diff=lfs merge=lfs -text
39
+ img/img12.png filter=lfs diff=lfs merge=lfs -text
40
+ img/img13.png filter=lfs diff=lfs merge=lfs -text
41
+ img/img14.png filter=lfs diff=lfs merge=lfs -text
42
+ img/img15.png filter=lfs diff=lfs merge=lfs -text
43
+ img/img16.png filter=lfs diff=lfs merge=lfs -text
44
+ img/img17.png filter=lfs diff=lfs merge=lfs -text
45
+ img/img18.png filter=lfs diff=lfs merge=lfs -text
46
+ img/img19.png filter=lfs diff=lfs merge=lfs -text
47
+ img/img2.png filter=lfs diff=lfs merge=lfs -text
48
+ img/img20.png filter=lfs diff=lfs merge=lfs -text
49
+ img/img21.png filter=lfs diff=lfs merge=lfs -text
50
+ img/img22.png filter=lfs diff=lfs merge=lfs -text
51
+ img/img23.png filter=lfs diff=lfs merge=lfs -text
52
+ img/img24.png filter=lfs diff=lfs merge=lfs -text
53
+ img/img25.png filter=lfs diff=lfs merge=lfs -text
54
+ img/img26.png filter=lfs diff=lfs merge=lfs -text
55
+ img/img27.png filter=lfs diff=lfs merge=lfs -text
56
+ img/img28.png filter=lfs diff=lfs merge=lfs -text
57
+ img/img3.png filter=lfs diff=lfs merge=lfs -text
58
+ img/img4.png filter=lfs diff=lfs merge=lfs -text
59
+ img/img5.png filter=lfs diff=lfs merge=lfs -text
60
+ img/img6.png filter=lfs diff=lfs merge=lfs -text
61
+ img/img7.png filter=lfs diff=lfs merge=lfs -text
62
+ img/img8.png filter=lfs diff=lfs merge=lfs -text
63
+ img/img9.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment variables
2
+ .env
3
+ .env.local
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ env/
12
+ venv/
13
+ venv311/
14
+ ENV/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Logs
27
+ *.log
28
+
29
+ # Generated files
30
+ *.zip
31
+
32
+ # Organized directories
33
+ docs/
34
+ tests/
35
+ development/
36
+ temp/
37
+
38
+ # Gradio cache
39
+ .gradio/
40
+ flagged/
41
+
42
+ # Development images (except secret.png)
43
+ *.png
44
+ !secret.png
45
+ *.jpg
46
+ *.jpeg
47
+ *.gif
48
+
49
+ # Claude local files
50
+ .claude/
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Chat UI Helper
3
+ emoji: πŸ’»
4
+ colorFrom: gray
5
+ colorTo: red
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: true
9
+ thumbnail: >-
10
+ https://cdn-uploads.huggingface.co/production/uploads/65a0caa15dfd8b9b1f3aa3d3/8cruZmIioPrYoTN8o-wcE.png
11
+ short_description: Configure, download, and deploy a simple chat interface
12
+ license: gpl-3.0
13
+ sdk_version: 5.37.0
14
+ ---
15
+
16
+ # Chat UI Helper
17
+
18
+ A Gradio-based tool for generating and configuring chat interfaces for HuggingFace Spaces. Create deployable packages with custom assistants and web scraping capabilities.
19
+
20
+ ## Features
21
+
22
+ ### Spaces Configuration
23
+ - **Custom Assistant Creation**: Define role, purpose, audience, and tasks
24
+ - **Template System**: Choose from research assistant template or build from scratch
25
+ - **Tool Integration**: Optional dynamic URL fetching
26
+ - **Access Control**: Secure access code protection for educational use
27
+ - **Complete Deployment Package**: Generates app.py, requirements.txt, and config.json
28
+
29
+ ### Chat Support
30
+ - **Expert Guidance**: Get personalized help with Gradio configurations
31
+ - **Context-Aware**: URL grounding for informed responses about HuggingFace Spaces
32
+ - **Deployment Assistance**: Troubleshooting and best practices
33
+
34
+ ## Quick Start
35
+
36
+ ### Running Locally
37
+ ```bash
38
+ pip install -r requirements.txt
39
+ python app.py
40
+ ```
41
+
42
+ ### For Chat Support (Optional)
43
+ Set your OpenRouter API key as a secret:
44
+ - Go to Settings β†’ Variables and secrets
45
+ - Add secret: `OPENROUTER_API_KEY`
46
+
47
+ ## Generated Space Features
48
+
49
+ Each generated space includes:
50
+ - **OpenRouter API Integration**: Support for multiple LLM models
51
+ - **Web Scraping**: Simple HTTP requests with BeautifulSoup for URL content fetching
52
+ - **Access Control**: Environment-based student access codes
53
+ - **Modern UI**: Gradio 5.x ChatInterface with proper message formatting
54
+
55
+ ## Architecture
56
+
57
+ - **Main Application**: `app.py` with three-tab interface
58
+ - **Web Scraping**: HTTP requests with BeautifulSoup for content extraction
59
+ - **Template Generation**: Complete HuggingFace Space creation
60
+
61
+ For detailed development guidance, see [CLAUDE.md](CLAUDE.md).
app.py ADDED
@@ -0,0 +1,2104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ warnings.filterwarnings("ignore", message="The 'tuples' format for chatbot messages is deprecated")
3
+
4
+ # Ensure we're using Gradio 4.x
5
+ import gradio as gr
6
+ print(f"Gradio version: {gr.__version__}")
7
+ import json
8
+ import zipfile
9
+ import io
10
+ import os
11
+ from datetime import datetime
12
+ from dotenv import load_dotenv
13
+ import requests
14
+ from bs4 import BeautifulSoup
15
+ import tempfile
16
+ from pathlib import Path
17
+ from support_docs import create_support_docs, export_conversation_to_markdown
18
+
19
+ # Simple URL content fetching using requests and BeautifulSoup
20
+ def get_grounding_context_simple(urls):
21
+ """Fetch grounding context using enhanced HTTP requests"""
22
+ if not urls:
23
+ return ""
24
+
25
+ context_parts = []
26
+ for i, url in enumerate(urls, 1):
27
+ if url and url.strip():
28
+ # Use enhanced URL extraction for any URLs within the URL text
29
+ extracted_urls = extract_urls_from_text(url.strip())
30
+ target_url = extracted_urls[0] if extracted_urls else url.strip()
31
+
32
+ content = enhanced_fetch_url_content(target_url)
33
+ context_parts.append(f"Context from URL {i} ({target_url}):\n{content}")
34
+
35
+ if context_parts:
36
+ return "\n\n" + "\n\n".join(context_parts) + "\n\n"
37
+ return ""
38
+
39
+
40
+ # Load environment variables from .env file
41
+ load_dotenv()
42
+
43
+ # Utility functions
44
+ import re
45
+
46
+ def extract_urls_from_text(text):
47
+ """Extract URLs from text using regex with enhanced validation"""
48
+ url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+'
49
+ urls = re.findall(url_pattern, text)
50
+
51
+ # Basic URL validation and cleanup
52
+ validated_urls = []
53
+ for url in urls:
54
+ # Remove trailing punctuation that might be captured
55
+ url = url.rstrip('.,!?;:')
56
+ # Basic domain validation
57
+ if '.' in url and len(url) > 10:
58
+ validated_urls.append(url)
59
+
60
+ return validated_urls
61
+
62
+ def validate_url_domain(url):
63
+ """Basic URL domain validation"""
64
+ try:
65
+ from urllib.parse import urlparse
66
+ parsed = urlparse(url)
67
+ # Check for valid domain structure
68
+ if parsed.netloc and '.' in parsed.netloc:
69
+ return True
70
+ except:
71
+ pass
72
+ return False
73
+
74
+ def enhanced_fetch_url_content(url, enable_search_validation=False):
75
+ """Enhanced URL content fetching with optional search validation"""
76
+ if not validate_url_domain(url):
77
+ return f"Invalid URL format: {url}"
78
+
79
+ try:
80
+ # Enhanced headers for better compatibility
81
+ headers = {
82
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
83
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
84
+ 'Accept-Language': 'en-US,en;q=0.5',
85
+ 'Accept-Encoding': 'gzip, deflate',
86
+ 'Connection': 'keep-alive'
87
+ }
88
+
89
+ response = requests.get(url, timeout=15, headers=headers)
90
+ response.raise_for_status()
91
+ soup = BeautifulSoup(response.content, 'html.parser')
92
+
93
+ # Enhanced content cleaning
94
+ for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]):
95
+ element.decompose()
96
+
97
+ # Extract main content preferentially
98
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or soup
99
+ text = main_content.get_text()
100
+
101
+ # Enhanced text cleaning
102
+ lines = (line.strip() for line in text.splitlines())
103
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
104
+ text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2)
105
+
106
+ # Smart truncation - try to end at sentence boundaries
107
+ if len(text) > 4000:
108
+ truncated = text[:4000]
109
+ last_period = truncated.rfind('.')
110
+ if last_period > 3000: # If we can find a reasonable sentence break
111
+ text = truncated[:last_period + 1]
112
+ else:
113
+ text = truncated + "..."
114
+
115
+ return text if text.strip() else "No readable content found at this URL"
116
+
117
+ except requests.exceptions.Timeout:
118
+ return f"Timeout error fetching {{url}} (15s limit exceeded)"
119
+ except requests.exceptions.RequestException as e:
120
+ return f"Error fetching {{url}}: {{str(e)}}"
121
+ except Exception as e:
122
+ return f"Error processing content from {{url}}: {{str(e)}}"
123
+
124
+ # Template for generated space app (based on mvp_simple.py)
125
+ SPACE_TEMPLATE = '''import gradio as gr
126
+ import tempfile
127
+ import os
128
+ import requests
129
+ import json
130
+ import re
131
+ from bs4 import BeautifulSoup
132
+ from datetime import datetime
133
+ import urllib.parse
134
+
135
+
136
+ # Configuration
137
+ SPACE_NAME = "{name}"
138
+ SPACE_DESCRIPTION = "{description}"
139
+ SYSTEM_PROMPT = """{system_prompt}"""
140
+ MODEL = "{model}"
141
+ GROUNDING_URLS = {grounding_urls}
142
+ # Get access code from environment variable for security
143
+ # If SPACE_ACCESS_CODE is not set, no access control is applied
144
+ ACCESS_CODE = os.environ.get("SPACE_ACCESS_CODE")
145
+ ENABLE_DYNAMIC_URLS = {enable_dynamic_urls}
146
+
147
+ # Get API key from environment - customizable variable name with validation
148
+ API_KEY = os.environ.get("{api_key_var}")
149
+ if API_KEY:
150
+ API_KEY = API_KEY.strip() # Remove any whitespace
151
+ if not API_KEY: # Check if empty after stripping
152
+ API_KEY = None
153
+
154
+ # API Key validation and logging
155
+ def validate_api_key():
156
+ """Validate API key configuration with detailed logging"""
157
+ if not API_KEY:
158
+ print(f"⚠️ API KEY CONFIGURATION ERROR:")
159
+ print(f" Variable name: {api_key_var}")
160
+ print(f" Status: Not set or empty")
161
+ print(f" Action needed: Set '{api_key_var}' in HuggingFace Space secrets")
162
+ print(f" Expected format: sk-or-xxxxxxxxxx")
163
+ return False
164
+ elif not API_KEY.startswith('sk-or-'):
165
+ print(f"⚠️ API KEY FORMAT WARNING:")
166
+ print(f" Variable name: {api_key_var}")
167
+ print(f" Current value: {{API_KEY[:10]}}..." if len(API_KEY) > 10 else API_KEY)
168
+ print(f" Expected format: sk-or-xxxxxxxxxx")
169
+ print(f" Note: OpenRouter keys should start with 'sk-or-'")
170
+ return True # Still try to use it
171
+ else:
172
+ print(f"βœ… API Key configured successfully")
173
+ print(f" Variable: {api_key_var}")
174
+ print(f" Format: Valid OpenRouter key")
175
+ return True
176
+
177
+ # Validate on startup
178
+ try:
179
+ API_KEY_VALID = validate_api_key()
180
+ except NameError:
181
+ # During template generation, API_KEY might not be defined yet
182
+ API_KEY_VALID = False
183
+
184
+ def validate_url_domain(url):
185
+ """Basic URL domain validation"""
186
+ try:
187
+ from urllib.parse import urlparse
188
+ parsed = urlparse(url)
189
+ # Check for valid domain structure
190
+ if parsed.netloc and '.' in parsed.netloc:
191
+ return True
192
+ except:
193
+ pass
194
+ return False
195
+
196
+ def fetch_url_content(url):
197
+ """Enhanced URL content fetching with improved compatibility and error handling"""
198
+ if not validate_url_domain(url):
199
+ return f"Invalid URL format: {{url}}"
200
+
201
+ try:
202
+ # Enhanced headers for better compatibility
203
+ headers = {{
204
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
205
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
206
+ 'Accept-Language': 'en-US,en;q=0.5',
207
+ 'Accept-Encoding': 'gzip, deflate',
208
+ 'Connection': 'keep-alive'
209
+ }}
210
+
211
+ response = requests.get(url, timeout=15, headers=headers)
212
+ response.raise_for_status()
213
+ soup = BeautifulSoup(response.content, 'html.parser')
214
+
215
+ # Enhanced content cleaning
216
+ for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]):
217
+ element.decompose()
218
+
219
+ # Extract main content preferentially
220
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or soup
221
+ text = main_content.get_text()
222
+
223
+ # Enhanced text cleaning
224
+ lines = (line.strip() for line in text.splitlines())
225
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
226
+ text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2)
227
+
228
+ # Smart truncation - try to end at sentence boundaries
229
+ if len(text) > 4000:
230
+ truncated = text[:4000]
231
+ last_period = truncated.rfind('.')
232
+ if last_period > 3000: # If we can find a reasonable sentence break
233
+ text = truncated[:last_period + 1]
234
+ else:
235
+ text = truncated + "..."
236
+
237
+ return text if text.strip() else "No readable content found at this URL"
238
+
239
+ except requests.exceptions.Timeout:
240
+ return f"Timeout error fetching {{url}} (15s limit exceeded)"
241
+ except requests.exceptions.RequestException as e:
242
+ return f"Error fetching {{url}}: {{str(e)}}"
243
+ except Exception as e:
244
+ return f"Error processing content from {{url}}: {{str(e)}}"
245
+
246
+ def extract_urls_from_text(text):
247
+ """Extract URLs from text using regex with enhanced validation"""
248
+ import re
249
+ url_pattern = r'https?://[^\\s<>"{{}}|\\\\^`\\[\\]"]+'
250
+ urls = re.findall(url_pattern, text)
251
+
252
+ # Basic URL validation and cleanup
253
+ validated_urls = []
254
+ for url in urls:
255
+ # Remove trailing punctuation that might be captured
256
+ url = url.rstrip('.,!?;:')
257
+ # Basic domain validation
258
+ if '.' in url and len(url) > 10:
259
+ validated_urls.append(url)
260
+
261
+ return validated_urls
262
+
263
+ # Global cache for URL content to avoid re-crawling in generated spaces
264
+ _url_content_cache = {{}}
265
+
266
+ def get_grounding_context():
267
+ """Fetch context from grounding URLs with caching"""
268
+ if not GROUNDING_URLS:
269
+ return ""
270
+
271
+ # Create cache key from URLs
272
+ cache_key = tuple(sorted([url for url in GROUNDING_URLS if url and url.strip()]))
273
+
274
+ # Check cache first
275
+ if cache_key in _url_content_cache:
276
+ return _url_content_cache[cache_key]
277
+
278
+ context_parts = []
279
+ for i, url in enumerate(GROUNDING_URLS, 1):
280
+ if url.strip():
281
+ content = fetch_url_content(url.strip())
282
+ # Add priority indicators
283
+ priority_label = "PRIMARY" if i <= 2 else "SECONDARY"
284
+ context_parts.append(f"[{{priority_label}}] Context from URL {{i}} ({{url}}):\\n{{content}}")
285
+
286
+ if context_parts:
287
+ result = "\\n\\n" + "\\n\\n".join(context_parts) + "\\n\\n"
288
+ else:
289
+ result = ""
290
+
291
+ # Cache the result
292
+ _url_content_cache[cache_key] = result
293
+ return result
294
+
295
+ def export_conversation_to_markdown(conversation_history):
296
+ """Export conversation history to markdown format"""
297
+ if not conversation_history:
298
+ return "No conversation to export."
299
+
300
+ markdown_content = f"""# Conversation Export
301
+ Generated on: {{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}}
302
+
303
+ ---
304
+
305
+ """
306
+
307
+ message_pair_count = 0
308
+ for i, message in enumerate(conversation_history):
309
+ if isinstance(message, dict):
310
+ role = message.get('role', 'unknown')
311
+ content = message.get('content', '')
312
+
313
+ if role == 'user':
314
+ message_pair_count += 1
315
+ markdown_content += f"## User Message {{message_pair_count}}\\n\\n{{content}}\\n\\n"
316
+ elif role == 'assistant':
317
+ markdown_content += f"## Assistant Response {{message_pair_count}}\\n\\n{{content}}\\n\\n---\\n\\n"
318
+ elif isinstance(message, (list, tuple)) and len(message) >= 2:
319
+ # Handle legacy tuple format: ["user msg", "assistant msg"]
320
+ message_pair_count += 1
321
+ user_msg, assistant_msg = message[0], message[1]
322
+ if user_msg:
323
+ markdown_content += f"## User Message {{message_pair_count}}\\n\\n{{user_msg}}\\n\\n"
324
+ if assistant_msg:
325
+ markdown_content += f"## Assistant Response {{message_pair_count}}\\n\\n{{assistant_msg}}\\n\\n---\\n\\n"
326
+
327
+ return markdown_content
328
+
329
+
330
+ def generate_response(message, history):
331
+ """Generate response using OpenRouter API"""
332
+
333
+ # Enhanced API key validation with helpful messages
334
+ if not API_KEY:
335
+ error_msg = f"πŸ”‘ **API Key Required**\\n\\n"
336
+ error_msg += f"Please configure your OpenRouter API key:\\n"
337
+ error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\\n"
338
+ error_msg += f"2. Click 'Variables and secrets'\\n"
339
+ error_msg += f"3. Add secret: **{api_key_var}**\\n"
340
+ error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\\n\\n"
341
+ error_msg += f"Get your API key at: https://openrouter.ai/keys"
342
+ print(f"❌ API request failed: No API key configured for {api_key_var}")
343
+ return error_msg
344
+
345
+ # Get grounding context
346
+ grounding_context = get_grounding_context()
347
+
348
+
349
+ # If dynamic URLs are enabled, check message for URLs to fetch
350
+ if ENABLE_DYNAMIC_URLS:
351
+ urls_in_message = extract_urls_from_text(message)
352
+ if urls_in_message:
353
+ # Fetch content from URLs mentioned in the message
354
+ dynamic_context_parts = []
355
+ for url in urls_in_message[:3]: # Limit to 3 URLs per message
356
+ content = fetch_url_content(url)
357
+ dynamic_context_parts.append(f"\\n\\nDynamic context from {{url}}:\\n{{content}}")
358
+ if dynamic_context_parts:
359
+ grounding_context += "\\n".join(dynamic_context_parts)
360
+
361
+ # Build enhanced system prompt with grounding context
362
+ enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
363
+
364
+ # Build messages array for the API
365
+ messages = [{{"role": "system", "content": enhanced_system_prompt}}]
366
+
367
+ # Add conversation history - handle both modern messages format and legacy tuples
368
+ for chat in history:
369
+ if isinstance(chat, dict):
370
+ # Modern format: {{"role": "user", "content": "..."}} or {{"role": "assistant", "content": "..."}}
371
+ messages.append(chat)
372
+ elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
373
+ # Legacy format: ["user msg", "assistant msg"] or ("user msg", "assistant msg")
374
+ user_msg, assistant_msg = chat[0], chat[1]
375
+ if user_msg:
376
+ messages.append({{"role": "user", "content": user_msg}})
377
+ if assistant_msg:
378
+ messages.append({{"role": "assistant", "content": assistant_msg}})
379
+
380
+ # Add current message
381
+ messages.append({{"role": "user", "content": message}})
382
+
383
+ # Make API request with enhanced error handling
384
+ try:
385
+ print(f"πŸ”„ Making API request to OpenRouter...")
386
+ print(f" Model: {{MODEL}}")
387
+ print(f" Messages: {{len(messages)}} in conversation")
388
+
389
+ response = requests.post(
390
+ url="https://openrouter.ai/api/v1/chat/completions",
391
+ headers={{
392
+ "Authorization": f"Bearer {{API_KEY}}",
393
+ "Content-Type": "application/json",
394
+ "HTTP-Referer": "https://huggingface.co", # Required by some providers
395
+ "X-Title": "HuggingFace Space" # Helpful for tracking
396
+ }},
397
+ json={{
398
+ "model": MODEL,
399
+ "messages": messages,
400
+ "temperature": {temperature},
401
+ "max_tokens": {max_tokens}
402
+ }},
403
+ timeout=30
404
+ )
405
+
406
+ print(f"πŸ“‘ API Response: {{response.status_code}}")
407
+
408
+ if response.status_code == 200:
409
+ try:
410
+ result = response.json()
411
+
412
+ # Enhanced validation of API response structure
413
+ if 'choices' not in result or not result['choices']:
414
+ print(f"⚠️ API response missing choices: {{result}}")
415
+ return "API Error: No response choices available"
416
+ elif 'message' not in result['choices'][0]:
417
+ print(f"⚠️ API response missing message: {{result}}")
418
+ return "API Error: No message in response"
419
+ elif 'content' not in result['choices'][0]['message']:
420
+ print(f"⚠️ API response missing content: {{result}}")
421
+ return "API Error: No content in message"
422
+ else:
423
+ content = result['choices'][0]['message']['content']
424
+
425
+ # Check for empty content
426
+ if not content or content.strip() == "":
427
+ print(f"⚠️ API returned empty content")
428
+ return "API Error: Empty response content"
429
+
430
+ print(f"βœ… API request successful")
431
+ return content
432
+
433
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
434
+ print(f"❌ Failed to parse API response: {{str(e)}}")
435
+ return f"API Error: Failed to parse response - {{str(e)}}"
436
+ elif response.status_code == 401:
437
+ error_msg = f"πŸ” **Authentication Error**\\n\\n"
438
+ error_msg += f"Your API key appears to be invalid or expired.\\n\\n"
439
+ error_msg += f"**Troubleshooting:**\\n"
440
+ error_msg += f"1. Check that your **{api_key_var}** secret is set correctly\\n"
441
+ error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\\n"
442
+ error_msg += f"3. Ensure your key starts with `sk-or-`\\n"
443
+ error_msg += f"4. Check that you have credits on your OpenRouter account"
444
+ print(f"❌ API authentication failed: {{response.status_code}} - {{response.text[:200]}}")
445
+ return error_msg
446
+ elif response.status_code == 429:
447
+ error_msg = f"⏱️ **Rate Limit Exceeded**\\n\\n"
448
+ error_msg += f"Too many requests. Please wait a moment and try again.\\n\\n"
449
+ error_msg += f"**Troubleshooting:**\\n"
450
+ error_msg += f"1. Wait 30-60 seconds before trying again\\n"
451
+ error_msg += f"2. Check your OpenRouter usage limits\\n"
452
+ error_msg += f"3. Consider upgrading your OpenRouter plan"
453
+ print(f"❌ Rate limit exceeded: {{response.status_code}}")
454
+ return error_msg
455
+ elif response.status_code == 400:
456
+ try:
457
+ error_data = response.json()
458
+ error_message = error_data.get('error', {{}}).get('message', 'Unknown error')
459
+ except:
460
+ error_message = response.text
461
+
462
+ error_msg = f"⚠️ **Request Error**\\n\\n"
463
+ error_msg += f"The API request was invalid:\\n"
464
+ error_msg += f"`{{error_message}}`\\n\\n"
465
+ if "model" in error_message.lower():
466
+ error_msg += f"**Model Issue:** The model `{{MODEL}}` may not be available.\\n"
467
+ error_msg += f"Try switching to a different model in your Space configuration."
468
+ print(f"❌ Bad request: {{response.status_code}} - {{error_message}}")
469
+ return error_msg
470
+ else:
471
+ error_msg = f"🚫 **API Error {{response.status_code}}**\\n\\n"
472
+ error_msg += f"An unexpected error occurred. Please try again.\\n\\n"
473
+ error_msg += f"If this persists, check:\\n"
474
+ error_msg += f"1. OpenRouter service status\\n"
475
+ error_msg += f"2. Your API key and credits\\n"
476
+ error_msg += f"3. The model availability"
477
+ print(f"❌ API error: {{response.status_code}} - {{response.text[:200]}}")
478
+ return error_msg
479
+
480
+ except requests.exceptions.Timeout:
481
+ error_msg = f"⏰ **Request Timeout**\\n\\n"
482
+ error_msg += f"The API request took too long (30s limit).\\n\\n"
483
+ error_msg += f"**Troubleshooting:**\\n"
484
+ error_msg += f"1. Try again with a shorter message\\n"
485
+ error_msg += f"2. Check your internet connection\\n"
486
+ error_msg += f"3. Try a different model"
487
+ print(f"❌ Request timeout after 30 seconds")
488
+ return error_msg
489
+ except requests.exceptions.ConnectionError:
490
+ error_msg = f"🌐 **Connection Error**\\n\\n"
491
+ error_msg += f"Could not connect to OpenRouter API.\\n\\n"
492
+ error_msg += f"**Troubleshooting:**\\n"
493
+ error_msg += f"1. Check your internet connection\\n"
494
+ error_msg += f"2. Check OpenRouter service status\\n"
495
+ error_msg += f"3. Try again in a few moments"
496
+ print(f"❌ Connection error to OpenRouter API")
497
+ return error_msg
498
+ except Exception as e:
499
+ error_msg = f"❌ **Unexpected Error**\\n\\n"
500
+ error_msg += f"An unexpected error occurred:\\n"
501
+ error_msg += f"`{{str(e)}}`\\n\\n"
502
+ error_msg += f"Please try again or contact support if this persists."
503
+ print(f"❌ Unexpected error: {{str(e)}}")
504
+ return error_msg
505
+
506
+ # Access code verification
507
+ access_granted = gr.State(False)
508
+ _access_granted_global = False # Global fallback
509
+
510
+ def verify_access_code(code):
511
+ \"\"\"Verify the access code\"\"\"
512
+ global _access_granted_global
513
+ if ACCESS_CODE is None:
514
+ _access_granted_global = True
515
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
516
+
517
+ if code == ACCESS_CODE:
518
+ _access_granted_global = True
519
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
520
+ else:
521
+ _access_granted_global = False
522
+ return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False)
523
+
524
+ def protected_generate_response(message, history):
525
+ \"\"\"Protected response function that checks access\"\"\"
526
+ # Check if access is granted via the global variable
527
+ if ACCESS_CODE is not None and not _access_granted_global:
528
+ return "Please enter the access code to continue."
529
+ return generate_response(message, history)
530
+
531
+ # Global variable to store chat history for export
532
+ chat_history_store = []
533
+
534
+ def store_and_generate_response(message, history):
535
+ \"\"\"Wrapper function that stores history and generates response\"\"\"
536
+ global chat_history_store
537
+
538
+ # Generate response using the protected function
539
+ response = protected_generate_response(message, history)
540
+
541
+ # Convert current history to the format we need for export
542
+ # history comes in as [["user1", "bot1"], ["user2", "bot2"], ...]
543
+ chat_history_store = []
544
+ if history:
545
+ for exchange in history:
546
+ if isinstance(exchange, (list, tuple)) and len(exchange) >= 2:
547
+ chat_history_store.append({{"role": "user", "content": exchange[0]}})
548
+ chat_history_store.append({{"role": "assistant", "content": exchange[1]}})
549
+
550
+ # Add the current exchange
551
+ chat_history_store.append({{"role": "user", "content": message}})
552
+ chat_history_store.append({{"role": "assistant", "content": response}})
553
+
554
+ return response
555
+
556
+ def export_current_conversation():
557
+ \"\"\"Export the current conversation\"\"\"
558
+ if not chat_history_store:
559
+ return gr.update(visible=False)
560
+
561
+ markdown_content = export_conversation_to_markdown(chat_history_store)
562
+
563
+ # Save to temporary file
564
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
565
+ f.write(markdown_content)
566
+ temp_file = f.name
567
+
568
+ return gr.update(value=temp_file, visible=True)
569
+
570
+ def export_conversation(history):
571
+ \"\"\"Export conversation to markdown file\"\"\"
572
+ if not history:
573
+ return gr.update(visible=False)
574
+
575
+ markdown_content = export_conversation_to_markdown(history)
576
+
577
+ # Save to temporary file
578
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
579
+ f.write(markdown_content)
580
+ temp_file = f.name
581
+
582
+ return gr.update(value=temp_file, visible=True)
583
+
584
+ # Configuration status display
585
+ def get_configuration_status():
586
+ \"\"\"Generate a configuration status message for display\"\"\"
587
+ status_parts = []
588
+
589
+ if API_KEY_VALID:
590
+ status_parts.append("βœ… **API Key:** Configured and valid")
591
+ else:
592
+ status_parts.append("❌ **API Key:** Not configured - Set `{api_key_var}` in Space secrets")
593
+
594
+ status_parts.append(f"πŸ€– **Model:** {{MODEL}}")
595
+ status_parts.append(f"🌑️ **Temperature:** {temperature}")
596
+ status_parts.append(f"πŸ“ **Max Tokens:** {max_tokens}")
597
+
598
+ # URL Grounding details
599
+ if GROUNDING_URLS:
600
+ status_parts.append(f"πŸ”— **URL Grounding:** {{len(GROUNDING_URLS)}} URLs configured")
601
+ # Show first few URLs as examples
602
+ for i, url in enumerate(GROUNDING_URLS[:3], 1):
603
+ priority_label = "Primary" if i <= 2 else "Secondary"
604
+ status_parts.append(f" - [{{priority_label}}] {{url}}")
605
+ if len(GROUNDING_URLS) > 3:
606
+ status_parts.append(f" - ... and {{len(GROUNDING_URLS) - 3}} more URLs")
607
+ else:
608
+ status_parts.append("πŸ”— **URL Grounding:** No URLs configured")
609
+
610
+ if ENABLE_DYNAMIC_URLS:
611
+ status_parts.append("πŸ”„ **Dynamic URLs:** Enabled")
612
+ else:
613
+ status_parts.append("πŸ”„ **Dynamic URLs:** Disabled")
614
+
615
+ if ACCESS_CODE is not None:
616
+ status_parts.append("πŸ” **Access Control:** Enabled")
617
+ else:
618
+ status_parts.append("🌐 **Access:** Public Chatbot")
619
+
620
+ # System Prompt (add at the end)
621
+ status_parts.append("") # Empty line for spacing
622
+ status_parts.append("**System Prompt:**")
623
+ status_parts.append(f"{{SYSTEM_PROMPT}}")
624
+
625
+ return "\\n".join(status_parts)
626
+
627
+ # Create interface with access code protection
628
+ with gr.Blocks(title=SPACE_NAME) as demo:
629
+ gr.Markdown(f"# {{SPACE_NAME}}")
630
+ gr.Markdown(SPACE_DESCRIPTION)
631
+
632
+ # Access code section (shown only if ACCESS_CODE is set)
633
+ with gr.Column(visible=(ACCESS_CODE is not None)) as access_section:
634
+ gr.Markdown("### πŸ” Access Required")
635
+ gr.Markdown("Please enter the access code provided by your instructor:")
636
+
637
+ access_input = gr.Textbox(
638
+ label="Access Code",
639
+ placeholder="Enter access code...",
640
+ type="password"
641
+ )
642
+ access_btn = gr.Button("Submit", variant="primary")
643
+ access_error = gr.Markdown(visible=False)
644
+
645
+ # Main chat interface (hidden until access granted)
646
+ with gr.Column(visible=(ACCESS_CODE is None)) as chat_section:
647
+ chat_interface = gr.ChatInterface(
648
+ fn=store_and_generate_response, # Use wrapper function to store history
649
+ title="", # Title already shown above
650
+ description="", # Description already shown above
651
+ examples={examples},
652
+ type="messages" # Use modern message format for better compatibility
653
+ )
654
+
655
+ # Export functionality
656
+ with gr.Row():
657
+ export_btn = gr.Button("πŸ“₯ Export Conversation", variant="secondary", size="sm")
658
+ export_file = gr.File(label="Download Conversation", visible=False)
659
+
660
+ # Connect export functionality
661
+ export_btn.click(
662
+ export_current_conversation,
663
+ outputs=[export_file]
664
+ )
665
+
666
+ # Configuration status (always visible)
667
+ with gr.Accordion("πŸ“Š Configuration Status", open=not API_KEY_VALID):
668
+ gr.Markdown(get_configuration_status())
669
+
670
+ # Connect access verification
671
+ if ACCESS_CODE is not None:
672
+ access_btn.click(
673
+ verify_access_code,
674
+ inputs=[access_input],
675
+ outputs=[access_error, chat_section, access_granted]
676
+ )
677
+ access_input.submit(
678
+ verify_access_code,
679
+ inputs=[access_input],
680
+ outputs=[access_error, chat_section, access_granted]
681
+ )
682
+
683
+ if __name__ == "__main__":
684
+ demo.launch()
685
+ '''
686
+
687
+ # Available models - Updated with valid OpenRouter model IDs
688
+ MODELS = [
689
+ "google/gemini-2.0-flash-001", # Fast, reliable, general tasks
690
+ "google/gemma-3-27b-it", # High-performance open model
691
+ "anthropic/claude-3.5-haiku", # Complex reasoning and analysis
692
+ "openai/gpt-4o-mini-search-preview", # Balanced performance and cost with search
693
+ "openai/gpt-4.1-nano", # Lightweight OpenAI model
694
+ "nvidia/llama-3.1-nemotron-70b-instruct", # Large open-source model
695
+ "mistralai/devstral-small" # Coding-focused model
696
+ ]
697
+
698
+
699
+ def get_grounding_context(urls):
700
+ """Fetch context from grounding URLs"""
701
+ if not urls:
702
+ return ""
703
+
704
+ context_parts = []
705
+ for i, url in enumerate(urls, 1):
706
+ if url and url.strip():
707
+ content = enhanced_fetch_url_content(url.strip())
708
+ # Add priority indicators
709
+ priority_label = "PRIMARY" if i <= 2 else "SECONDARY"
710
+ context_parts.append(f"[{priority_label}] Context from URL {i} ({url}):\n{content}")
711
+
712
+ if context_parts:
713
+ return "\n\n" + "\n\n".join(context_parts) + "\n\n"
714
+ return ""
715
+
716
+ def create_readme(config):
717
+ """Generate README with deployment instructions and proper HF Spaces YAML header"""
718
+ # Ensure short_description is a proper string and within HF's 60-character limit
719
+ description = config.get('description', '') or 'AI chat interface'
720
+ if len(description) > 60:
721
+ short_desc = description[:57] + '...' # 57 + 3 = 60 characters total
722
+ else:
723
+ short_desc = description
724
+
725
+ return f"""---
726
+ title: {config['name']}
727
+ emoji: πŸ€–
728
+ colorFrom: blue
729
+ colorTo: red
730
+ sdk: gradio
731
+ sdk_version: 5.38.0
732
+ app_file: app.py
733
+ pinned: false
734
+ license: mit
735
+ short_description: "{short_desc}"
736
+ ---
737
+
738
+ # {config['name']}
739
+
740
+ {config['description']}
741
+
742
+ ## Quick Deploy to HuggingFace Spaces
743
+
744
+ ### Step 1: Create the Space
745
+ 1. Go to https://huggingface.co/spaces
746
+ 2. Click "Create new Space"
747
+ 3. Choose a name for your Space
748
+ 4. Select **Gradio** as the SDK
749
+ 5. Set visibility (Public/Private)
750
+ 6. Click "Create Space"
751
+
752
+ ### Step 2: Upload Files
753
+ 1. In your new Space, click "Files" tab
754
+ 2. Upload these files from the zip:
755
+ - `app.py`
756
+ - `requirements.txt`
757
+ 3. Wait for "Building" to complete
758
+
759
+ ### Step 3: Add API Key
760
+ 1. Go to Settings (gear icon)
761
+ 2. Click "Variables and secrets"
762
+ 3. Click "New secret"
763
+ 4. Name: `{config['api_key_var']}`
764
+ 5. Value: Your OpenRouter API key
765
+ 6. Click "Add"
766
+
767
+ {f'''### Step 4: Configure Access Control
768
+ Your Space is configured with access code protection. Students will need to enter the access code to use the chatbot.
769
+
770
+ 1. Go to Settings (gear icon)
771
+ 2. Click "Variables and secrets"
772
+ 3. Click "New secret"
773
+ 4. Name: `SPACE_ACCESS_CODE`
774
+ 5. Value: `{config['access_code']}`
775
+ 6. Click "Add"
776
+
777
+ **Important**: The access code is now stored securely as an environment variable and is not visible in your app code.
778
+
779
+ To disable access protection:
780
+ 1. Go to Settings β†’ Variables and secrets
781
+ 2. Delete the `SPACE_ACCESS_CODE` secret
782
+ 3. The Space will rebuild automatically with no access protection
783
+
784
+ ''' if config['access_code'] else ''}
785
+
786
+ ### Step {4 if not config['access_code'] else 5}: Get Your API Key
787
+ 1. Go to https://openrouter.ai/keys
788
+ 2. Sign up/login if needed
789
+ 3. Click "Create Key"
790
+ 4. Copy the key (starts with `sk-or-`)
791
+
792
+ ### Step {5 if not config['access_code'] else 6}: Test Your Space
793
+ - Go back to "App" tab
794
+ - Your Space should be running!
795
+ - Try the example prompts or ask a question
796
+
797
+ ## Configuration
798
+
799
+ - **Model**: {config['model']}
800
+ - **Temperature**: {config['temperature']}
801
+ - **Max Tokens**: {config['max_tokens']}
802
+ - **API Key Variable**: {config['api_key_var']}"""
803
+
804
+ # Add optional configuration items
805
+ if config['access_code']:
806
+ readme_content += f"""
807
+ - **Access Code**: {config['access_code']} (Students need this to access the chatbot)"""
808
+
809
+ if config.get('enable_dynamic_urls'):
810
+ readme_content += """
811
+ - **Dynamic URL Fetching**: Enabled (Assistant can fetch URLs mentioned in conversations)"""
812
+
813
+ readme_content += f"""
814
+
815
+ ## Customization
816
+
817
+ To modify your Space:
818
+ 1. Edit `app.py` in your Space
819
+ 2. Update configuration variables at the top
820
+ 3. Changes deploy automatically
821
+
822
+ ## Troubleshooting
823
+
824
+ - **"Please set your {config['api_key_var']}"**: Add the secret in Space settings
825
+ - **Error 401**: Invalid API key or no credits
826
+ - **Error 429**: Rate limit - wait and try again
827
+ - **Build failed**: Check requirements.txt formatting
828
+
829
+ ## More Help
830
+
831
+ - HuggingFace Spaces: https://huggingface.co/docs/hub/spaces
832
+ - OpenRouter Docs: https://openrouter.ai/docs
833
+ - Gradio Docs: https://gradio.app/docs
834
+
835
+ ---
836
+
837
+ Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} with Chat U/I Helper
838
+ """
839
+
840
+ return readme_content
841
+
842
+ def create_requirements():
843
+ """Generate requirements.txt with latest versions"""
844
+ return "gradio>=5.38.0\nrequests>=2.32.3\nbeautifulsoup4>=4.12.3\npython-dotenv>=1.0.0"
845
+
846
+ def generate_zip(name, description, system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code_field="", enable_dynamic_urls=False, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""):
847
+ """Generate deployable zip file"""
848
+
849
+ # Process examples
850
+ if examples_text and examples_text.strip():
851
+ examples_list = [ex.strip() for ex in examples_text.split('\n') if ex.strip()]
852
+ examples_python = repr(examples_list) # Convert to Python literal representation
853
+ else:
854
+ examples_python = repr([
855
+ "Hello! How can you help me?",
856
+ "Tell me something interesting",
857
+ "What can you do?"
858
+ ])
859
+
860
+ # Process grounding URLs
861
+ grounding_urls = []
862
+ for url in [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]:
863
+ if url and url.strip():
864
+ grounding_urls.append(url.strip())
865
+
866
+ # Use the provided system prompt directly
867
+
868
+ # Create config
869
+ config = {
870
+ 'name': name,
871
+ 'description': description,
872
+ 'system_prompt': system_prompt,
873
+ 'model': model,
874
+ 'api_key_var': api_key_var,
875
+ 'temperature': temperature,
876
+ 'max_tokens': int(max_tokens),
877
+ 'examples': examples_python,
878
+ 'grounding_urls': json.dumps(grounding_urls),
879
+ 'enable_dynamic_urls': enable_dynamic_urls
880
+ }
881
+
882
+ # Generate files
883
+ app_content = SPACE_TEMPLATE.format(**config)
884
+ # Pass empty access_code to README since user will configure it in HF Spaces
885
+ readme_config = config.copy()
886
+ readme_config['access_code'] = "" # Always empty since user configures in HF Spaces
887
+ readme_content = create_readme(readme_config)
888
+ requirements_content = create_requirements()
889
+
890
+ # Create zip file with clean naming
891
+ filename = f"{name.lower().replace(' ', '_').replace('-', '_')}.zip"
892
+
893
+ # Create zip in memory and save to disk
894
+ zip_buffer = io.BytesIO()
895
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
896
+ zip_file.writestr('app.py', app_content)
897
+ zip_file.writestr('requirements.txt', requirements_content)
898
+ zip_file.writestr('README.md', readme_content)
899
+ zip_file.writestr('config.json', json.dumps(config, indent=2))
900
+
901
+ # Write zip to file
902
+ zip_buffer.seek(0)
903
+ with open(filename, 'wb') as f:
904
+ f.write(zip_buffer.getvalue())
905
+
906
+ return filename
907
+
908
+ # Define callback functions outside the interface
909
+
910
+ def update_sandbox_preview(config_data):
911
+ """Update the sandbox preview with generated content"""
912
+ if not config_data:
913
+ return "Generate a space configuration to see preview here.", "<div style='text-align: center; padding: 50px; color: #666;'>No preview available</div>"
914
+
915
+ # Create preview info
916
+ preview_text = f"""**Space Configuration:**
917
+ - **Name:** {config_data.get('name', 'N/A')}
918
+ - **Model:** {config_data.get('model', 'N/A')}
919
+ - **Temperature:** {config_data.get('temperature', 'N/A')}
920
+ - **Max Tokens:** {config_data.get('max_tokens', 'N/A')}
921
+ - **Dynamic URLs:** {'βœ… Enabled' if config_data.get('enable_dynamic_urls') else '❌ Disabled'}
922
+
923
+ **System Prompt Preview:**
924
+ > {config_data.get('system_prompt', 'No system prompt configured')[:500]}{'...' if len(config_data.get('system_prompt', '')) > 500 else ''}
925
+
926
+ **Deployment Package:** `{config_data.get('filename', 'Not generated')}`"""
927
+
928
+ # Create a basic HTML preview of the chat interface
929
+ preview_html = f"""
930
+ <div style="border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: white;">
931
+ <h3 style="margin-top: 0; color: #333;">{config_data.get('name', 'Chat Interface')}</h3>
932
+ <p style="color: #666; margin-bottom: 20px;">{config_data.get('description', 'A customizable AI chat interface')}</p>
933
+
934
+ <div style="border: 1px solid #ccc; border-radius: 4px; background: white; min-height: 200px; padding: 15px; margin-bottom: 15px;">
935
+ <div style="color: #888; text-align: center; padding: 50px 0;">Chat Interface Preview</div>
936
+ <div style="background: #f0f8ff; padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 3px solid #0066cc;">
937
+ <strong>Assistant:</strong> Hello! I'm ready to help you. How can I assist you today?
938
+ </div>
939
+ </div>
940
+
941
+ <div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; background: white;">
942
+ <input type="text" placeholder="Type your message here..." style="width: 70%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px;">
943
+ <button style="padding: 8px 15px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
944
+ </div>
945
+
946
+ <div style="margin-top: 15px; padding: 10px; background: white; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 12px; color: #666;">
947
+ <strong>Configuration:</strong> Model: {config_data.get('model', 'N/A')} | Temperature: {config_data.get('temperature', 'N/A')} | Max Tokens: {config_data.get('max_tokens', 'N/A')}
948
+ </div>
949
+ </div>
950
+ """
951
+
952
+ return preview_text, preview_html
953
+
954
+ def on_preview_combined(name, description, system_prompt, model, temperature, max_tokens, examples_text, enable_dynamic_urls, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""):
955
+ """Generate configuration and return preview updates"""
956
+ if not name or not name.strip():
957
+ return (
958
+ {},
959
+ gr.update(value="**Error:** Please provide a Space Title to preview", visible=True),
960
+ gr.update(visible=False),
961
+ gr.update(value="Configuration will appear here after preview generation."),
962
+ *[gr.update() for _ in range(10)], # 10 URL updates
963
+ gr.update(), # preview_add_url_btn
964
+ gr.update(), # preview_remove_url_btn
965
+ 2, # preview_url_count
966
+ *[gr.update(visible=False) for _ in range(3)] # 3 example button updates
967
+ )
968
+
969
+ try:
970
+ # Use the system prompt directly (template selector already updates it)
971
+ if not system_prompt or not system_prompt.strip():
972
+ return (
973
+ {},
974
+ gr.update(value="**Error:** Please provide a System Prompt for the assistant", visible=True),
975
+ gr.update(visible=False),
976
+ gr.update(value="Configuration will appear here after preview generation."),
977
+ *[gr.update() for _ in range(10)], # 10 URL updates
978
+ gr.update(), # preview_add_url_btn
979
+ gr.update(), # preview_remove_url_btn
980
+ 2, # preview_url_count
981
+ *[gr.update(visible=False) for _ in range(3)] # 3 example button updates
982
+ )
983
+
984
+ final_system_prompt = system_prompt.strip()
985
+
986
+ # Process examples like the deployment package
987
+ if examples_text and examples_text.strip():
988
+ examples_list = [ex.strip() for ex in examples_text.split('\n') if ex.strip()]
989
+ else:
990
+ examples_list = [
991
+ "Hello! How can you help me?",
992
+ "Tell me something interesting",
993
+ "What can you do?"
994
+ ]
995
+
996
+ # Create configuration for preview
997
+ config_data = {
998
+ 'name': name,
999
+ 'description': description,
1000
+ 'system_prompt': final_system_prompt,
1001
+ 'model': model,
1002
+ 'temperature': temperature,
1003
+ 'max_tokens': max_tokens,
1004
+ 'enable_dynamic_urls': enable_dynamic_urls,
1005
+ 'url1': url1,
1006
+ 'url2': url2,
1007
+ 'url3': url3,
1008
+ 'url4': url4,
1009
+ 'url5': url5,
1010
+ 'url6': url6,
1011
+ 'url7': url7,
1012
+ 'url8': url8,
1013
+ 'url9': url9,
1014
+ 'url10': url10,
1015
+ 'examples_text': examples_text,
1016
+ 'examples_list': examples_list, # Processed examples for preview
1017
+ 'preview_ready': True
1018
+ }
1019
+
1020
+ # Generate preview displays with example prompts
1021
+ examples_preview = "\n".join([f"β€’ {ex}" for ex in examples_list[:3]]) # Show first 3 examples
1022
+
1023
+ preview_text = f"""**{name}** is ready to test. Use the example prompts below or type your own message."""
1024
+ config_display = f"""> **Configuration**:
1025
+ - **Name:** {name}
1026
+ - **Description:** {description or 'No description provided'}
1027
+ - **Model:** {model}
1028
+ - **Temperature:** {temperature}
1029
+ - **Max Response Tokens:** {max_tokens}
1030
+
1031
+ **System Prompt:**
1032
+ {final_system_prompt}
1033
+
1034
+ **Example Prompts:**
1035
+ {examples_text if examples_text and examples_text.strip() else 'No example prompts configured'}
1036
+ """
1037
+
1038
+ # Show success notification
1039
+ gr.Info(f"βœ… Preview generated successfully for '{name}'! Switch to Preview tab.")
1040
+
1041
+ # Determine how many URLs are configured
1042
+ all_urls = [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]
1043
+ url_count = 2 # Start with 2 (always visible)
1044
+ for i, url in enumerate(all_urls[2:], start=3): # Check urls 3-10
1045
+ if url and url.strip():
1046
+ url_count = i
1047
+ else:
1048
+ break
1049
+
1050
+ # Create URL updates for all preview URLs
1051
+ url_updates = []
1052
+ for i in range(1, 11): # URLs 1-10
1053
+ url_value = all_urls[i-1] if i <= len(all_urls) else ""
1054
+ if i <= 2: # URLs 1-2 are always visible
1055
+ url_updates.append(gr.update(value=url_value))
1056
+ else: # URLs 3-10
1057
+ url_updates.append(gr.update(value=url_value, visible=(i <= url_count)))
1058
+
1059
+ # Update button states
1060
+ secondary_count = url_count - 2 # Number of secondary URLs
1061
+ if url_count >= 10:
1062
+ preview_add_btn_update = gr.update(value="Max Secondary URLs (8/8)", interactive=False)
1063
+ else:
1064
+ preview_add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)", interactive=True)
1065
+
1066
+ preview_remove_btn_update = gr.update(visible=(url_count > 2))
1067
+
1068
+ # Update example buttons
1069
+ example_btn_updates = []
1070
+ for i in range(3):
1071
+ if i < len(examples_list):
1072
+ # Add click icon and truncate text nicely
1073
+ btn_text = f"πŸ’¬ {examples_list[i][:45]}{'...' if len(examples_list[i]) > 45 else ''}"
1074
+ example_btn_updates.append(gr.update(value=btn_text, visible=True))
1075
+ else:
1076
+ example_btn_updates.append(gr.update(visible=False))
1077
+
1078
+ return (
1079
+ config_data,
1080
+ gr.update(value=preview_text, visible=True),
1081
+ gr.update(visible=True),
1082
+ gr.update(value=config_display),
1083
+ *url_updates, # Unpack all 10 URL updates
1084
+ preview_add_btn_update,
1085
+ preview_remove_btn_update,
1086
+ url_count,
1087
+ *example_btn_updates # Add example button updates
1088
+ )
1089
+
1090
+ except Exception as e:
1091
+ return (
1092
+ {},
1093
+ gr.update(value=f"**Error:** {str(e)}", visible=True),
1094
+ gr.update(visible=False),
1095
+ gr.update(value="Configuration will appear here after preview generation."),
1096
+ *[gr.update() for _ in range(10)], # 10 URL updates
1097
+ gr.update(), # preview_add_url_btn
1098
+ gr.update(), # preview_remove_url_btn
1099
+ 2, # preview_url_count
1100
+ *[gr.update(visible=False) for _ in range(3)] # 3 example button updates
1101
+ )
1102
+
1103
+ def update_preview_display(config_data):
1104
+ """Update preview display based on config data"""
1105
+ if not config_data or not config_data.get('preview_ready'):
1106
+ return (
1107
+ gr.update(value="**Status:** Configure your space in the Configuration tab and click 'Preview Deployment Package' to see your assistant here.", visible=True),
1108
+ gr.update(visible=False),
1109
+ gr.update(value="Configuration will appear here after preview generation.")
1110
+ )
1111
+
1112
+ # Generate example prompts display
1113
+ examples_list = config_data.get('examples_list', [])
1114
+ examples_preview = "\n".join([f"β€’ {ex}" for ex in examples_list[:3]]) # Show first 3 examples
1115
+
1116
+ preview_text = f"""**{config_data['name']}** is ready to test. Use the example prompts below or type your own message."""
1117
+
1118
+ config_display = f"""### Current Configuration
1119
+
1120
+ **Space Details:**
1121
+ - **Name:** {config_data['name']}
1122
+ - **Description:** {config_data.get('description', 'No description provided')}
1123
+
1124
+ **Model Settings:**
1125
+ - **Model:** {config_data['model']}
1126
+ - **Temperature:** {config_data['temperature']}
1127
+ - **Max Response Tokens:** {config_data['max_tokens']}
1128
+
1129
+ **Features:**
1130
+ - **Dynamic URL Fetching:** {'βœ… Enabled' if config_data['enable_dynamic_urls'] else '❌ Disabled'}
1131
+
1132
+ **System Prompt:**
1133
+ {config_data['system_prompt']}
1134
+
1135
+ **Example Prompts:**
1136
+ {config_data.get('examples_text', 'No example prompts configured') if config_data.get('examples_text', '').strip() else 'No example prompts configured'}
1137
+ """
1138
+
1139
+ return (
1140
+ gr.update(value=preview_text, visible=True),
1141
+ gr.update(visible=True),
1142
+ gr.update(value=config_display)
1143
+ )
1144
+
1145
+ def preview_chat_response(message, history, config_data, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""):
1146
+ """Generate response for preview chat using actual OpenRouter API"""
1147
+ if not config_data or not message:
1148
+ return "", history
1149
+
1150
+ # Get API key from environment
1151
+ api_key = os.environ.get("OPENROUTER_API_KEY")
1152
+
1153
+ if not api_key:
1154
+ response = f"""πŸ”‘ **API Key Required for Preview**
1155
+
1156
+ To test your assistant with real API responses, please:
1157
+
1158
+ 1. Get your OpenRouter API key from: https://openrouter.ai/keys
1159
+ 2. Set it as an environment variable: `export OPENROUTER_API_KEY=your_key_here`
1160
+ 3. Or add it to your `.env` file: `OPENROUTER_API_KEY=your_key_here`
1161
+
1162
+ **Your Configuration:**
1163
+ - **Name:** {config_data.get('name', 'your assistant')}
1164
+ - **Model:** {config_data.get('model', 'unknown model')}
1165
+ - **Temperature:** {config_data.get('temperature', 0.7)}
1166
+ - **Max Tokens:** {config_data.get('max_tokens', 500)}
1167
+
1168
+ **System Prompt Preview:**
1169
+ {config_data.get('system_prompt', '')[:200]}{'...' if len(config_data.get('system_prompt', '')) > 200 else ''}
1170
+
1171
+ Once you set your API key, you'll be able to test real conversations in this preview."""
1172
+ history.append({"role": "user", "content": message})
1173
+ history.append({"role": "assistant", "content": response})
1174
+ return "", history
1175
+
1176
+ try:
1177
+ # Get grounding context from URLs - prioritize config_data URLs, fallback to preview tab URLs
1178
+ config_urls = [
1179
+ config_data.get('url1', ''),
1180
+ config_data.get('url2', ''),
1181
+ config_data.get('url3', ''),
1182
+ config_data.get('url4', ''),
1183
+ config_data.get('url5', ''),
1184
+ config_data.get('url6', ''),
1185
+ config_data.get('url7', ''),
1186
+ config_data.get('url8', ''),
1187
+ config_data.get('url9', ''),
1188
+ config_data.get('url10', '')
1189
+ ]
1190
+ # Use config URLs if available, otherwise use preview tab URLs
1191
+ grounding_urls = config_urls if any(url for url in config_urls if url) else [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]
1192
+ grounding_context = get_cached_grounding_context([url for url in grounding_urls if url and url.strip()])
1193
+
1194
+
1195
+ # If dynamic URLs are enabled, check message for URLs to fetch
1196
+ dynamic_context = ""
1197
+ if config_data.get('enable_dynamic_urls'):
1198
+ urls_in_message = extract_urls_from_text(message)
1199
+ if urls_in_message:
1200
+ dynamic_context_parts = []
1201
+ for url in urls_in_message[:3]: # Increased limit to 3 URLs with enhanced processing
1202
+ content = enhanced_fetch_url_content(url)
1203
+ dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
1204
+ if dynamic_context_parts:
1205
+ dynamic_context = "\n".join(dynamic_context_parts)
1206
+
1207
+ # Build enhanced system prompt with all contexts
1208
+ enhanced_system_prompt = config_data.get('system_prompt', '') + grounding_context + dynamic_context
1209
+
1210
+ # Build messages array for the API
1211
+ messages = [{"role": "system", "content": enhanced_system_prompt}]
1212
+
1213
+ # Add conversation history - handle both formats for backwards compatibility
1214
+ for chat in history:
1215
+ if isinstance(chat, dict):
1216
+ # New format: {"role": "user", "content": "..."}
1217
+ messages.append(chat)
1218
+ elif isinstance(chat, list) and len(chat) >= 2:
1219
+ # Legacy format: [user_msg, assistant_msg]
1220
+ user_msg, assistant_msg = chat[0], chat[1]
1221
+ if user_msg:
1222
+ messages.append({"role": "user", "content": user_msg})
1223
+ if assistant_msg:
1224
+ messages.append({"role": "assistant", "content": assistant_msg})
1225
+
1226
+ # Add current message
1227
+ messages.append({"role": "user", "content": message})
1228
+
1229
+ # Debug: Log the request being sent
1230
+ request_payload = {
1231
+ "model": config_data.get('model', 'google/gemini-2.0-flash-001'),
1232
+ "messages": messages,
1233
+ "temperature": config_data.get('temperature', 0.7),
1234
+ "max_tokens": config_data.get('max_tokens', 500),
1235
+ "tools": None # Explicitly disable tool/function calling
1236
+ }
1237
+
1238
+ # Make API request to OpenRouter
1239
+ response = requests.post(
1240
+ url="https://openrouter.ai/api/v1/chat/completions",
1241
+ headers={
1242
+ "Authorization": f"Bearer {api_key}",
1243
+ "Content-Type": "application/json"
1244
+ },
1245
+ json=request_payload,
1246
+ timeout=30
1247
+ )
1248
+
1249
+ if response.status_code == 200:
1250
+ try:
1251
+ response_data = response.json()
1252
+
1253
+ # Check if response has expected structure
1254
+ if 'choices' not in response_data or not response_data['choices']:
1255
+ assistant_response = f"[Preview Debug] No choices in API response. Response: {response_data}"
1256
+ elif 'message' not in response_data['choices'][0]:
1257
+ assistant_response = f"[Preview Debug] No message in first choice. Response: {response_data}"
1258
+ elif 'content' not in response_data['choices'][0]['message']:
1259
+ assistant_response = f"[Preview Debug] No content in message. Response: {response_data}"
1260
+ else:
1261
+ assistant_content = response_data['choices'][0]['message']['content']
1262
+
1263
+ # Debug: Check if content is empty
1264
+ if not assistant_content or assistant_content.strip() == "":
1265
+ assistant_response = f"[Preview Debug] Empty content from API. Messages sent: {len(messages)} messages, last user message: '{message}', model: {request_payload['model']}"
1266
+ else:
1267
+ # Use the content directly - no preview indicator needed
1268
+ assistant_response = assistant_content
1269
+
1270
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
1271
+ assistant_response = f"[Preview Error] Failed to parse API response: {str(e)}. Raw response: {response.text[:500]}"
1272
+ else:
1273
+ assistant_response = f"[Preview Error] API Error: {response.status_code} - {response.text[:500]}"
1274
+
1275
+ except Exception as e:
1276
+ assistant_response = f"[Preview Error] {str(e)}"
1277
+
1278
+ # Return in the new messages format for Gradio 5.x
1279
+ history.append({"role": "user", "content": message})
1280
+ history.append({"role": "assistant", "content": assistant_response})
1281
+ return "", history
1282
+
1283
+ def clear_preview_chat():
1284
+ """Clear preview chat"""
1285
+ return "", []
1286
+
1287
+ def set_example_prompt(example_text):
1288
+ """Set example prompt in the text input"""
1289
+ return example_text
1290
+
1291
+ def export_preview_conversation(history, config_data=None):
1292
+ """Export preview conversation to markdown"""
1293
+ if not history:
1294
+ return gr.update(visible=False)
1295
+
1296
+ markdown_content = export_conversation_to_markdown(history, config_data)
1297
+
1298
+ # Save to temporary file
1299
+ import tempfile
1300
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
1301
+ f.write(markdown_content)
1302
+ temp_file = f.name
1303
+
1304
+ return gr.update(value=temp_file, visible=True)
1305
+
1306
+ def on_generate(name, description, system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code, enable_dynamic_urls, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10):
1307
+ if not name or not name.strip():
1308
+ return gr.update(value="Error: Please provide a Space Title", visible=True), gr.update(visible=False), {}
1309
+
1310
+
1311
+ try:
1312
+ # Use the system prompt directly (template selector already updates it)
1313
+ if not system_prompt or not system_prompt.strip():
1314
+ return gr.update(value="Error: Please provide a System Prompt for the assistant", visible=True), gr.update(visible=False), {}
1315
+
1316
+ final_system_prompt = system_prompt.strip()
1317
+
1318
+ filename = generate_zip(name, description, final_system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code, enable_dynamic_urls, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10)
1319
+
1320
+ success_msg = f"""**Deployment package ready!**
1321
+
1322
+ **File**: `{filename}`
1323
+
1324
+ **What's included:**
1325
+ - `app.py` - Ready-to-deploy chat interface (Gradio 5.38.0)
1326
+ - `requirements.txt` - Latest dependencies
1327
+ - `README.md` - HuggingFace Spaces configuration & instructions
1328
+ - `config.json` - Configuration backup
1329
+
1330
+ **Next steps:**
1331
+ 1. Download the zip file below
1332
+ 2. Go to https://huggingface.co/spaces and create a new Space
1333
+ 3. Upload ALL files from the zip to your Space (including README.md)
1334
+ 4. Set your `{api_key_var}` secret in Space settings
1335
+
1336
+ **Your Space will be live in minutes!**"""
1337
+
1338
+ # Update sandbox preview
1339
+ config_data = {
1340
+ 'name': name,
1341
+ 'description': description,
1342
+ 'system_prompt': final_system_prompt,
1343
+ 'model': model,
1344
+ 'temperature': temperature,
1345
+ 'max_tokens': max_tokens,
1346
+ 'enable_dynamic_urls': enable_dynamic_urls,
1347
+ 'filename': filename
1348
+ }
1349
+
1350
+ return gr.update(value=success_msg, visible=True), gr.update(value=filename, visible=True), config_data
1351
+
1352
+ except Exception as e:
1353
+ return gr.update(value=f"Error: {str(e)}", visible=True), gr.update(visible=False), {}
1354
+
1355
+ # Global cache for URL content to avoid re-crawling
1356
+ url_content_cache = {}
1357
+
1358
+ def get_cached_grounding_context(urls):
1359
+ """Get grounding context with caching to avoid re-crawling same URLs"""
1360
+ if not urls:
1361
+ return ""
1362
+
1363
+ # Filter valid URLs
1364
+ valid_urls = [url for url in urls if url and url.strip()]
1365
+ if not valid_urls:
1366
+ return ""
1367
+
1368
+ # Create cache key from sorted URLs
1369
+ cache_key = tuple(sorted(valid_urls))
1370
+
1371
+ # Check if we already have this content cached
1372
+ if cache_key in url_content_cache:
1373
+ return url_content_cache[cache_key]
1374
+
1375
+ # If not cached, fetch using simple HTTP requests
1376
+ grounding_context = get_grounding_context_simple(valid_urls)
1377
+
1378
+ # Cache the result
1379
+ url_content_cache[cache_key] = grounding_context
1380
+
1381
+ return grounding_context
1382
+
1383
+
1384
+ def respond(message, chat_history, url1="", url2="", url3="", url4="", url5="", url6="", url7="", url8="", url9="", url10=""):
1385
+ # Make actual API request to OpenRouter
1386
+ import os
1387
+ import requests
1388
+
1389
+ # Get API key from environment
1390
+ api_key = os.environ.get("OPENROUTER_API_KEY")
1391
+
1392
+ if not api_key:
1393
+ response = "Please set your OPENROUTER_API_KEY in the Space settings to use the chat support."
1394
+ chat_history.append({"role": "user", "content": message})
1395
+ chat_history.append({"role": "assistant", "content": response})
1396
+ return "", chat_history
1397
+
1398
+ # Get grounding context from URLs using cached approach
1399
+ grounding_urls = [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]
1400
+ grounding_context = get_cached_grounding_context(grounding_urls)
1401
+
1402
+ # Build enhanced system prompt with grounding context
1403
+ base_system_prompt = """You are an expert assistant specializing in Gradio configurations for HuggingFace Spaces. You have deep knowledge of:
1404
+ - Gradio interface components and layouts
1405
+ - HuggingFace Spaces configuration (YAML frontmatter, secrets, environment variables)
1406
+ - Deployment best practices for Gradio apps on HuggingFace
1407
+ - Space settings, SDK versions, and hardware requirements
1408
+ - Troubleshooting common Gradio and HuggingFace Spaces issues
1409
+ - Integration with various APIs and models through Gradio interfaces
1410
+
1411
+ Provide specific, technical guidance focused on Gradio implementation details and HuggingFace Spaces deployment. Include code examples when relevant. Keep responses concise and actionable."""
1412
+
1413
+ enhanced_system_prompt = base_system_prompt + grounding_context
1414
+
1415
+ # Build conversation history for API
1416
+ messages = [{
1417
+ "role": "system",
1418
+ "content": enhanced_system_prompt
1419
+ }]
1420
+
1421
+ # Add conversation history - Support both new messages format and legacy tuple format
1422
+ for chat in chat_history:
1423
+ if isinstance(chat, dict):
1424
+ # New format: {"role": "user", "content": "..."}
1425
+ messages.append(chat)
1426
+ elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
1427
+ # Legacy format: ("user msg", "bot msg")
1428
+ user_msg, assistant_msg = chat[0], chat[1]
1429
+ if user_msg:
1430
+ messages.append({"role": "user", "content": user_msg})
1431
+ if assistant_msg:
1432
+ messages.append({"role": "assistant", "content": assistant_msg})
1433
+
1434
+ # Add current message
1435
+ messages.append({"role": "user", "content": message})
1436
+
1437
+ try:
1438
+ # Make API request to OpenRouter
1439
+ response = requests.post(
1440
+ url="https://openrouter.ai/api/v1/chat/completions",
1441
+ headers={
1442
+ "Authorization": f"Bearer {api_key}",
1443
+ "Content-Type": "application/json"
1444
+ },
1445
+ json={
1446
+ "model": "google/gemini-2.0-flash-001",
1447
+ "messages": messages,
1448
+ "temperature": 0.7,
1449
+ "max_tokens": 500
1450
+ }
1451
+ )
1452
+
1453
+ if response.status_code == 200:
1454
+ assistant_response = response.json()['choices'][0]['message']['content']
1455
+ else:
1456
+ assistant_response = f"Error: {response.status_code} - {response.text}"
1457
+
1458
+ except Exception as e:
1459
+ assistant_response = f"Error: {str(e)}"
1460
+
1461
+ chat_history.append({"role": "user", "content": message})
1462
+ chat_history.append({"role": "assistant", "content": assistant_response})
1463
+ return "", chat_history
1464
+
1465
+ def clear_chat():
1466
+ return "", []
1467
+
1468
+
1469
+
1470
+ def add_urls(count):
1471
+ """Show additional URL fields"""
1472
+ new_count = min(count + 1, 10)
1473
+
1474
+ # Create visibility updates for all URL fields (3-10)
1475
+ url_updates = []
1476
+ for i in range(3, 11): # URLs 3-10
1477
+ if i <= new_count:
1478
+ url_updates.append(gr.update(visible=True))
1479
+ else:
1480
+ url_updates.append(gr.update(visible=False))
1481
+
1482
+ # Update button states
1483
+ secondary_count = new_count - 2 # Number of secondary URLs
1484
+ if new_count >= 10:
1485
+ add_btn_update = gr.update(value="Max Secondary URLs (8/8)", interactive=False)
1486
+ else:
1487
+ add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)")
1488
+
1489
+ remove_btn_update = gr.update(visible=True)
1490
+
1491
+ return (*url_updates, add_btn_update, remove_btn_update, new_count)
1492
+
1493
+ def remove_urls(count):
1494
+ """Hide URL fields"""
1495
+ new_count = max(count - 1, 4)
1496
+
1497
+ # Create visibility updates for all URL fields (3-10)
1498
+ url_updates = []
1499
+ for i in range(3, 11): # URLs 3-10
1500
+ if i <= new_count:
1501
+ url_updates.append(gr.update(visible=True))
1502
+ else:
1503
+ url_updates.append(gr.update(visible=False, value=""))
1504
+
1505
+ # Update button states
1506
+ secondary_count = new_count - 2 # Number of secondary URLs
1507
+ add_btn_update = gr.update(value=f"+ Add Secondary URLs ({secondary_count}/8)", interactive=True)
1508
+
1509
+ if new_count <= 4:
1510
+ remove_btn_update = gr.update(visible=False)
1511
+ else:
1512
+ remove_btn_update = gr.update(visible=True)
1513
+
1514
+ return (*url_updates, add_btn_update, remove_btn_update, new_count)
1515
+
1516
+
1517
+
1518
+
1519
+ def toggle_template(template_choice, current_prompt, cached_custom_prompt):
1520
+ """Toggle between different assistant templates"""
1521
+ # If we're switching away from "None", cache the current custom prompt
1522
+ if template_choice != "None" and current_prompt and current_prompt.strip():
1523
+ # Check if the current prompt is not a template prompt
1524
+ research_prompt = "You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation."
1525
+ socratic_prompt = "You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinkingβ€”analysis, synthesis, or evaluationβ€”rather than recall."
1526
+
1527
+ # Only cache if it's not one of the template prompts
1528
+ if current_prompt != research_prompt and current_prompt != socratic_prompt:
1529
+ cached_custom_prompt = current_prompt
1530
+
1531
+ if template_choice == "Research Template":
1532
+ research_prompt = "You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation."
1533
+ return (
1534
+ gr.update(value=research_prompt), # Update main system prompt
1535
+ gr.update(value=True), # Enable dynamic URL fetching for research template
1536
+ cached_custom_prompt # Return the cached prompt
1537
+ )
1538
+ elif template_choice == "Socratic Template":
1539
+ socratic_prompt = "You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinkingβ€”analysis, synthesis, or evaluationβ€”rather than recall."
1540
+ return (
1541
+ gr.update(value=socratic_prompt), # Update main system prompt
1542
+ gr.update(value=False), # Socratic template doesn't need dynamic URLs by default
1543
+ cached_custom_prompt # Return the cached prompt
1544
+ )
1545
+ else: # "None" or any other value
1546
+ # Restore the cached custom prompt if we have one
1547
+ prompt_value = cached_custom_prompt if cached_custom_prompt else ""
1548
+ return (
1549
+ gr.update(value=prompt_value), # Restore cached prompt or clear
1550
+ gr.update(value=False), # Disable dynamic URL setting
1551
+ cached_custom_prompt # Return the cached prompt
1552
+ )
1553
+
1554
+
1555
+ # Create Gradio interface with proper tab structure and fixed configuration
1556
+ with gr.Blocks(
1557
+ title="Chat U/I Helper",
1558
+ css="""
1559
+ /* Custom CSS to fix styling issues */
1560
+ .gradio-container {
1561
+ max-width: 1200px !important;
1562
+ margin: 0 auto;
1563
+ }
1564
+
1565
+ /* Fix tab styling */
1566
+ .tab-nav {
1567
+ border-bottom: 1px solid #e0e0e0;
1568
+ }
1569
+
1570
+ /* Fix button styling */
1571
+ .btn {
1572
+ border-radius: 6px;
1573
+ }
1574
+
1575
+ /* Fix chat interface styling */
1576
+ .chat-interface {
1577
+ border-radius: 8px;
1578
+ border: 1px solid #e0e0e0;
1579
+ }
1580
+
1581
+ /* Hide gradio footer to avoid manifest issues */
1582
+ .gradio-footer {
1583
+ display: none !important;
1584
+ }
1585
+
1586
+ /* Fix accordion styling */
1587
+ .accordion {
1588
+ border: 1px solid #e0e0e0;
1589
+ border-radius: 6px;
1590
+ }
1591
+ """,
1592
+ theme="default",
1593
+ head="""
1594
+ <style>
1595
+ /* Additional head styles to prevent manifest issues */
1596
+ .gradio-app {
1597
+ background: #ffffff;
1598
+ }
1599
+ </style>
1600
+ """,
1601
+ js="""
1602
+ function() {
1603
+ // Prevent manifest.json requests and other common errors
1604
+ if (typeof window !== 'undefined') {
1605
+ // Override fetch to handle manifest.json requests
1606
+ const originalFetch = window.fetch;
1607
+ window.fetch = function(url, options) {
1608
+ // Handle both string URLs and URL objects
1609
+ const urlString = typeof url === 'string' ? url : url.toString();
1610
+
1611
+ if (urlString.includes('manifest.json')) {
1612
+ return Promise.resolve(new Response('{}', {
1613
+ status: 200,
1614
+ headers: { 'Content-Type': 'application/json' }
1615
+ }));
1616
+ }
1617
+
1618
+ // Handle favicon requests
1619
+ if (urlString.includes('favicon.ico')) {
1620
+ return Promise.resolve(new Response('', { status: 204 }));
1621
+ }
1622
+
1623
+ return originalFetch.apply(this, arguments);
1624
+ };
1625
+
1626
+ // Prevent postMessage origin errors
1627
+ window.addEventListener('message', function(event) {
1628
+ try {
1629
+ if (event.origin && event.origin !== window.location.origin) {
1630
+ event.stopImmediatePropagation();
1631
+ return false;
1632
+ }
1633
+ } catch (e) {
1634
+ // Silently ignore origin check errors
1635
+ }
1636
+ }, true);
1637
+
1638
+ // Prevent console errors from missing resources
1639
+ window.addEventListener('error', function(e) {
1640
+ if (e.target && e.target.src) {
1641
+ const src = e.target.src;
1642
+ if (src.includes('manifest.json') || src.includes('favicon.ico')) {
1643
+ e.preventDefault();
1644
+ return false;
1645
+ }
1646
+ }
1647
+ }, true);
1648
+
1649
+ // Override console.error to filter out known harmless errors
1650
+ const originalConsoleError = console.error;
1651
+ console.error = function(...args) {
1652
+ const message = args.join(' ');
1653
+ if (message.includes('manifest.json') ||
1654
+ message.includes('favicon.ico') ||
1655
+ message.includes('postMessage') ||
1656
+ message.includes('target origin')) {
1657
+ return; // Suppress these specific errors
1658
+ }
1659
+ originalConsoleError.apply(console, arguments);
1660
+ };
1661
+ }
1662
+ }
1663
+ """
1664
+ ) as demo:
1665
+ # Global state for cross-tab functionality
1666
+ sandbox_state = gr.State({})
1667
+ preview_config_state = gr.State({})
1668
+
1669
+ # Global status components that will be defined later
1670
+ preview_status = None
1671
+ preview_chat_section = None
1672
+ config_display = None
1673
+
1674
+ with gr.Tabs():
1675
+ with gr.Tab("Configuration"):
1676
+ gr.Markdown("# Spaces Configuration")
1677
+ gr.Markdown("Convert custom assistants from HuggingChat into chat interfaces with HuggingFace Spaces. Configure and download everything needed to deploy a simple HF space using Gradio.")
1678
+
1679
+ with gr.Column():
1680
+ name = gr.Textbox(
1681
+ label="Space Title",
1682
+ placeholder="My Course Helper",
1683
+ value="My Custom Space"
1684
+ )
1685
+
1686
+ description = gr.Textbox(
1687
+ label="Space Description",
1688
+ placeholder="A customizable AI chat interface for...",
1689
+ lines=2,
1690
+ value="",
1691
+ max_lines=2,
1692
+ max_length=57
1693
+ )
1694
+
1695
+ model = gr.Dropdown(
1696
+ label="Model",
1697
+ choices=MODELS,
1698
+ value=MODELS[0],
1699
+ info="Choose based on the context and purposes of your space"
1700
+ )
1701
+
1702
+ api_key_var = gr.Textbox(
1703
+ label="API Key Configuration (Required)",
1704
+ value="OPENROUTER_API_KEY",
1705
+ info="Set this secret in HuggingFace Space Settings β†’ Variables and secrets with your OpenRouter API key",
1706
+ interactive=False
1707
+ )
1708
+
1709
+ access_code = gr.Textbox(
1710
+ label="Space Access Code (Optional)",
1711
+ value="SPACE_ACCESS_CODE",
1712
+ info="Set this secret in HuggingFace Space Settings β†’ Variables and secrets to require an access code.",
1713
+ interactive=False
1714
+ )
1715
+
1716
+ with gr.Accordion("Assistant Configuration", open=True):
1717
+ gr.Markdown("Define the system prompt and assistant settings. You can use pre-configured templates or custom fields.")
1718
+
1719
+ # Template selection - moved before system prompt
1720
+ gr.Markdown("**Choose a Template:** Start with a pre-configured assistant personality or create your own custom prompt. Templates provide tested prompts for common educational use cases.")
1721
+
1722
+ with gr.Row():
1723
+ template_selector = gr.Radio(
1724
+ label="Assistant Template",
1725
+ choices=["None", "Research Template", "Socratic Template"],
1726
+ value="None",
1727
+ info="Select a template to auto-fill the system prompt, or choose 'None' for custom"
1728
+ )
1729
+
1730
+ # Main system prompt field - now after template selector
1731
+ system_prompt = gr.Textbox(
1732
+ label="System Prompt",
1733
+ placeholder="You are a helpful assistant that...",
1734
+ lines=4,
1735
+ value="",
1736
+ info="Define the assistant's role, purpose, and behavior. This will be auto-filled if you select a template."
1737
+ )
1738
+
1739
+
1740
+
1741
+
1742
+
1743
+ examples_text = gr.Textbox(
1744
+ label="Example Prompts (one per line)",
1745
+ placeholder="Can you analyze this research paper: https://example.com/paper.pdf\nWhat are the latest findings on climate change adaptation?\nHelp me fact-check claims about renewable energy efficiency",
1746
+ lines=3,
1747
+ info="These will appear as clickable examples in the chat interface"
1748
+ )
1749
+
1750
+ enable_dynamic_urls = gr.Checkbox(
1751
+ label="Enable Dynamic URL Fetching",
1752
+ value=True, # Enabled by default
1753
+ info="Allow the assistant to fetch additional URLs mentioned in conversations"
1754
+ )
1755
+
1756
+ with gr.Accordion("URL Grounding", open=True):
1757
+ gr.Markdown("Add URLs to provide context. Content will be fetched and added to the system prompt. You can add up to 10 URLs total (2 primary + 8 secondary).")
1758
+
1759
+ # Primary URLs section
1760
+ gr.Markdown("### 🎯 Primary URLs (Always Processed)")
1761
+ gr.Markdown("These URLs are processed first and given highest priority for context.")
1762
+
1763
+ url1 = gr.Textbox(
1764
+ label="Primary URL 1",
1765
+ placeholder="https://syllabus.edu/course (most important source)",
1766
+ info="Main reference document, syllabus, or primary source"
1767
+ )
1768
+
1769
+ url2 = gr.Textbox(
1770
+ label="Primary URL 2",
1771
+ placeholder="https://textbook.com/chapter (core material)",
1772
+ info="Secondary reference, textbook chapter, or key resource"
1773
+ )
1774
+
1775
+ # Secondary URLs section
1776
+ gr.Markdown("### πŸ“š Secondary URLs (Additional Context)")
1777
+ gr.Markdown("Additional sources for supplementary context and enhanced responses.")
1778
+
1779
+ url3 = gr.Textbox(
1780
+ label="Secondary URL 1",
1781
+ placeholder="https://example.com/supplementary",
1782
+ info="Additional reference or supplementary material",
1783
+ visible=True
1784
+ )
1785
+
1786
+ url4 = gr.Textbox(
1787
+ label="Secondary URL 2",
1788
+ placeholder="https://example.com/resources",
1789
+ info="Extra context or supporting documentation",
1790
+ visible=True
1791
+ )
1792
+
1793
+ url5 = gr.Textbox(
1794
+ label="Secondary URL 3",
1795
+ placeholder="https://example.com/guidelines",
1796
+ info="Additional guidelines or reference material",
1797
+ visible=False
1798
+ )
1799
+
1800
+ url6 = gr.Textbox(
1801
+ label="Secondary URL 4",
1802
+ placeholder="https://example.com/examples",
1803
+ info="Examples, case studies, or additional sources",
1804
+ visible=False
1805
+ )
1806
+
1807
+ url7 = gr.Textbox(
1808
+ label="Secondary URL 5",
1809
+ placeholder="https://example.com/research",
1810
+ info="Research papers or academic sources",
1811
+ visible=False
1812
+ )
1813
+
1814
+ url8 = gr.Textbox(
1815
+ label="Secondary URL 6",
1816
+ placeholder="https://example.com/documentation",
1817
+ info="Technical documentation or specifications",
1818
+ visible=False
1819
+ )
1820
+
1821
+ url9 = gr.Textbox(
1822
+ label="Secondary URL 7",
1823
+ placeholder="https://example.com/articles",
1824
+ info="Articles, blog posts, or news sources",
1825
+ visible=False
1826
+ )
1827
+
1828
+ url10 = gr.Textbox(
1829
+ label="Secondary URL 8",
1830
+ placeholder="https://example.com/misc",
1831
+ info="Miscellaneous sources or background material",
1832
+ visible=False
1833
+ )
1834
+
1835
+ # URL management buttons
1836
+ with gr.Row():
1837
+ add_url_btn = gr.Button("+ Add Secondary URLs (2/8)", size="sm")
1838
+ remove_url_btn = gr.Button("- Remove Secondary URLs", size="sm", visible=True)
1839
+ url_count = gr.State(4) # Track number of visible URLs
1840
+
1841
+ with gr.Accordion("Advanced Settings", open=False):
1842
+ with gr.Row():
1843
+ temperature = gr.Slider(
1844
+ label="Temperature",
1845
+ minimum=0,
1846
+ maximum=2,
1847
+ value=0.7,
1848
+ step=0.1,
1849
+ info="Higher = more creative, Lower = more focused"
1850
+ )
1851
+
1852
+ max_tokens = gr.Slider(
1853
+ label="Max Response Tokens",
1854
+ minimum=50,
1855
+ maximum=4096,
1856
+ value=750,
1857
+ step=50
1858
+ )
1859
+
1860
+ with gr.Row():
1861
+ preview_btn = gr.Button("Preview Deployment Package", variant="secondary")
1862
+ generate_btn = gr.Button("Generate Deployment Package", variant="primary")
1863
+
1864
+ status = gr.Markdown(visible=False)
1865
+ download_file = gr.File(label="Download your zip package", visible=False)
1866
+
1867
+ # State variable to cache custom system prompt
1868
+ custom_prompt_cache = gr.State("")
1869
+
1870
+ # Connect the template selector
1871
+ template_selector.change(
1872
+ toggle_template,
1873
+ inputs=[template_selector, system_prompt, custom_prompt_cache],
1874
+ outputs=[system_prompt, enable_dynamic_urls, custom_prompt_cache]
1875
+ )
1876
+
1877
+ # Web search checkbox is now just for enabling/disabling the feature
1878
+ # No additional UI elements needed since we rely on model capabilities
1879
+
1880
+
1881
+
1882
+ # Connect the URL management buttons
1883
+ add_url_btn.click(
1884
+ add_urls,
1885
+ inputs=[url_count],
1886
+ outputs=[url3, url4, url5, url6, url7, url8, url9, url10, add_url_btn, remove_url_btn, url_count]
1887
+ )
1888
+
1889
+ remove_url_btn.click(
1890
+ remove_urls,
1891
+ inputs=[url_count],
1892
+ outputs=[url3, url4, url5, url6, url7, url8, url9, url10, add_url_btn, remove_url_btn, url_count]
1893
+ )
1894
+
1895
+
1896
+
1897
+ # Connect the generate button
1898
+ generate_btn.click(
1899
+ on_generate,
1900
+ inputs=[name, description, system_prompt, model, api_key_var, temperature, max_tokens, examples_text, access_code, enable_dynamic_urls, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10],
1901
+ outputs=[status, download_file, sandbox_state]
1902
+ )
1903
+
1904
+
1905
+ with gr.Tab("Preview"):
1906
+ gr.Markdown("# Sandbox Preview")
1907
+ gr.Markdown("Test your assistant before deployment.")
1908
+
1909
+ with gr.Column():
1910
+ # Preview status - assign to global variable
1911
+ preview_status_comp = gr.Markdown("**Status:** Configure your space in the Configuration tab and click 'Preview Deployment Package' to see your assistant here.", visible=True)
1912
+
1913
+ # Simulated chat interface for preview using ChatInterface
1914
+ with gr.Column(visible=False) as preview_chat_section_comp:
1915
+ preview_chatbot = gr.Chatbot(
1916
+ value=[],
1917
+ label="Preview Chat Interface",
1918
+ height=400,
1919
+ type="messages"
1920
+ )
1921
+ # Example prompt buttons
1922
+ with gr.Row():
1923
+ preview_example_btn1 = gr.Button("", visible=False, size="sm", variant="secondary")
1924
+ preview_example_btn2 = gr.Button("", visible=False, size="sm", variant="secondary")
1925
+ preview_example_btn3 = gr.Button("", visible=False, size="sm", variant="secondary")
1926
+
1927
+ preview_msg = gr.Textbox(
1928
+ label="Test your assistant",
1929
+ placeholder="Type a message to test your assistant...",
1930
+ lines=2
1931
+ )
1932
+
1933
+ # URL context fields for preview testing
1934
+ with gr.Accordion("Test URL Context (Optional)", open=False):
1935
+ gr.Markdown("Test URL context grounding in the preview. Uses same priority system: 2 primary + 8 secondary URLs.")
1936
+
1937
+ # Primary URLs for preview testing
1938
+ gr.Markdown("**🎯 Primary URLs**")
1939
+ with gr.Row():
1940
+ preview_url1 = gr.Textbox(
1941
+ label="Primary URL 1",
1942
+ placeholder="https://syllabus.edu/course",
1943
+ scale=1
1944
+ )
1945
+ preview_url2 = gr.Textbox(
1946
+ label="Primary URL 2",
1947
+ placeholder="https://textbook.com/chapter",
1948
+ scale=1
1949
+ )
1950
+
1951
+ # Secondary URLs for preview testing
1952
+ gr.Markdown("**πŸ“š Secondary URLs**")
1953
+ with gr.Row():
1954
+ preview_url3 = gr.Textbox(
1955
+ label="Secondary URL 1",
1956
+ placeholder="https://example.com/supplementary",
1957
+ scale=1,
1958
+ visible=True
1959
+ )
1960
+ preview_url4 = gr.Textbox(
1961
+ label="Secondary URL 2",
1962
+ placeholder="https://example.com/resources",
1963
+ scale=1,
1964
+ visible=True
1965
+ )
1966
+
1967
+ with gr.Row():
1968
+ preview_url5 = gr.Textbox(
1969
+ label="Secondary URL 3",
1970
+ placeholder="https://example.com/guidelines",
1971
+ scale=1,
1972
+ visible=False
1973
+ )
1974
+ preview_url6 = gr.Textbox(
1975
+ label="Secondary URL 4",
1976
+ placeholder="https://example.com/examples",
1977
+ scale=1,
1978
+ visible=False
1979
+ )
1980
+
1981
+ with gr.Row():
1982
+ preview_url7 = gr.Textbox(
1983
+ label="Secondary URL 5",
1984
+ placeholder="https://example.com/research",
1985
+ scale=1,
1986
+ visible=False
1987
+ )
1988
+ preview_url8 = gr.Textbox(
1989
+ label="Secondary URL 6",
1990
+ placeholder="https://example.com/documentation",
1991
+ scale=1,
1992
+ visible=False
1993
+ )
1994
+
1995
+ with gr.Row():
1996
+ preview_url9 = gr.Textbox(
1997
+ label="Secondary URL 7",
1998
+ placeholder="https://example.com/articles",
1999
+ scale=1,
2000
+ visible=False
2001
+ )
2002
+ preview_url10 = gr.Textbox(
2003
+ label="Secondary URL 8",
2004
+ placeholder="https://example.com/misc",
2005
+ scale=1,
2006
+ visible=False
2007
+ )
2008
+
2009
+ # URL management for preview
2010
+ with gr.Row():
2011
+ preview_add_url_btn = gr.Button("+ Add Secondary URLs (2/8)", size="sm")
2012
+ preview_remove_url_btn = gr.Button("- Remove Secondary URLs", size="sm", visible=True)
2013
+ preview_url_count = gr.State(4)
2014
+
2015
+ with gr.Row():
2016
+ preview_send = gr.Button("Send", variant="primary")
2017
+ preview_clear = gr.Button("Clear")
2018
+ preview_export_btn = gr.Button("Export Conversation", variant="secondary")
2019
+
2020
+ # Export functionality
2021
+ export_file = gr.File(label="Download Conversation", visible=False)
2022
+
2023
+ # Configuration display - assign to global variable
2024
+ config_display_comp = gr.Markdown("Configuration will appear here after preview generation.")
2025
+
2026
+
2027
+
2028
+ # Connect preview chat functionality
2029
+ preview_send.click(
2030
+ preview_chat_response,
2031
+ inputs=[preview_msg, preview_chatbot, preview_config_state, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10],
2032
+ outputs=[preview_msg, preview_chatbot]
2033
+ )
2034
+
2035
+ preview_msg.submit(
2036
+ preview_chat_response,
2037
+ inputs=[preview_msg, preview_chatbot, preview_config_state, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10],
2038
+ outputs=[preview_msg, preview_chatbot]
2039
+ )
2040
+
2041
+ preview_clear.click(
2042
+ clear_preview_chat,
2043
+ outputs=[preview_msg, preview_chatbot]
2044
+ )
2045
+
2046
+ preview_export_btn.click(
2047
+ export_preview_conversation,
2048
+ inputs=[preview_chatbot],
2049
+ outputs=[export_file]
2050
+ )
2051
+
2052
+ # Connect preview URL management buttons
2053
+ preview_add_url_btn.click(
2054
+ add_urls,
2055
+ inputs=[preview_url_count],
2056
+ outputs=[preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count]
2057
+ )
2058
+
2059
+ preview_remove_url_btn.click(
2060
+ remove_urls,
2061
+ inputs=[preview_url_count],
2062
+ outputs=[preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count]
2063
+ )
2064
+
2065
+ # Connect example buttons to populate text input
2066
+ # Need to get the example text from the state
2067
+ def get_example_text(config_data, index):
2068
+ if config_data and config_data.get('examples_list'):
2069
+ examples = config_data['examples_list']
2070
+ if index < len(examples):
2071
+ return examples[index]
2072
+ return ""
2073
+
2074
+ preview_example_btn1.click(
2075
+ lambda config_data: get_example_text(config_data, 0),
2076
+ inputs=[preview_config_state],
2077
+ outputs=[preview_msg]
2078
+ )
2079
+
2080
+ preview_example_btn2.click(
2081
+ lambda config_data: get_example_text(config_data, 1),
2082
+ inputs=[preview_config_state],
2083
+ outputs=[preview_msg]
2084
+ )
2085
+
2086
+ preview_example_btn3.click(
2087
+ lambda config_data: get_example_text(config_data, 2),
2088
+ inputs=[preview_config_state],
2089
+ outputs=[preview_msg]
2090
+ )
2091
+
2092
+ with gr.Tab("Support"):
2093
+ create_support_docs()
2094
+
2095
+ # Connect cross-tab functionality after all components are defined
2096
+ preview_btn.click(
2097
+ on_preview_combined,
2098
+ inputs=[name, description, system_prompt, model, temperature, max_tokens, examples_text, enable_dynamic_urls, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10],
2099
+ outputs=[preview_config_state, preview_status_comp, preview_chat_section_comp, config_display_comp, preview_url1, preview_url2, preview_url3, preview_url4, preview_url5, preview_url6, preview_url7, preview_url8, preview_url9, preview_url10, preview_add_url_btn, preview_remove_url_btn, preview_url_count, preview_example_btn1, preview_example_btn2, preview_example_btn3]
2100
+ )
2101
+
2102
+ if __name__ == "__main__":
2103
+ # Use default Gradio launch settings for HuggingFace Spaces compatibility
2104
+ demo.launch(share=True)
img/img1.png ADDED

Git LFS Details

  • SHA256: dd005f48bc930f6ffc647497138a964ae795832ec2859a6c12b4ae81b1d3c592
  • Pointer size: 131 Bytes
  • Size of remote file: 895 kB
img/img10.png ADDED

Git LFS Details

  • SHA256: 335aaa8a843e65dfc09f9737a2a7369962d9be380c2d1b426d6793ef991eb980
  • Pointer size: 131 Bytes
  • Size of remote file: 459 kB
img/img11.png ADDED

Git LFS Details

  • SHA256: a13561c47ab94a304556579824326950f55085dcd737ff595a9b15c6aa58dbfd
  • Pointer size: 131 Bytes
  • Size of remote file: 390 kB
img/img12.png ADDED

Git LFS Details

  • SHA256: 81c6cf216aa08db9762d554e79a439cf82a7ac9b9be4b1cb3b84deeea5c0c1e9
  • Pointer size: 131 Bytes
  • Size of remote file: 768 kB
img/img13.png ADDED

Git LFS Details

  • SHA256: f6ce689038028849d83a27ddc75788b54f34d25fa068c7a6554ebf87abcf5199
  • Pointer size: 131 Bytes
  • Size of remote file: 909 kB
img/img14.png ADDED

Git LFS Details

  • SHA256: ff321e3f8c0022ef450eb72d4cb83f65be64a3b139364ca36764f83b1b7a6e64
  • Pointer size: 131 Bytes
  • Size of remote file: 841 kB
img/img15.png ADDED

Git LFS Details

  • SHA256: 46db07409fd975b57b57bce24ed736d2b36317ec050c7b8f3847d671900d804b
  • Pointer size: 131 Bytes
  • Size of remote file: 291 kB
img/img16.png ADDED

Git LFS Details

  • SHA256: a4a9816c501f876c281d1a17e85f656a9faf0b3cc65e5bce53f286377fbe225c
  • Pointer size: 131 Bytes
  • Size of remote file: 814 kB
img/img17.png ADDED

Git LFS Details

  • SHA256: 8d60144af5b5fb8d0a0ba4b2d7fe3e789741e9ee0379b84154fbb4314d6a5cee
  • Pointer size: 131 Bytes
  • Size of remote file: 790 kB
img/img18.png ADDED

Git LFS Details

  • SHA256: 7d8b0890e41fd01d34fb72dd7e12bc1a80fbecc6c9684c6f998b413ec1c956fa
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
img/img19.png ADDED

Git LFS Details

  • SHA256: 3e421fc8e8ae9be839184419dfef3ffadba2103689fa2ff61e1113218398243d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
img/img2.png ADDED

Git LFS Details

  • SHA256: 6fbdbe51ae84e8208fee429ad6ebd3ffae1918a0f3163c940d4837604ada6f63
  • Pointer size: 131 Bytes
  • Size of remote file: 767 kB
img/img20.png ADDED

Git LFS Details

  • SHA256: 0aeab478475bf27beaecb1b42de735f49f35d7478502207a3720b1c464dfa0af
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
img/img21.png ADDED

Git LFS Details

  • SHA256: 5276c990bb46b2aebdce645d6a3a01fe1c308539bc3e10974ab1ccc7e33a522c
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
img/img22.png ADDED

Git LFS Details

  • SHA256: c192eccbccc4a416c7796d413086477573a60b363dfdfcef5c58e219e92e3d3c
  • Pointer size: 131 Bytes
  • Size of remote file: 246 kB
img/img23.png ADDED

Git LFS Details

  • SHA256: 4b6e5109877e7c0ada897e833f770326f6d5e960a8faad233ee47d1cc479f508
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
img/img24.png ADDED

Git LFS Details

  • SHA256: 37bc0a433a4bb74abe15ea716be2633af6433b5ead1da1d14c2c1a1afc9a944d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
img/img25.png ADDED

Git LFS Details

  • SHA256: 9a4a7360c8375206f6490d7b542258bc739d7e16095954a0e67b11e5e7eebd24
  • Pointer size: 131 Bytes
  • Size of remote file: 246 kB
img/img26.png ADDED

Git LFS Details

  • SHA256: 88105198235e262f6f1482579d18316e605c44446328c7042fd3c99bceb58310
  • Pointer size: 131 Bytes
  • Size of remote file: 233 kB
img/img27.png ADDED

Git LFS Details

  • SHA256: 138d3c067ded6d82017e9b9901d8adc79b038c7df7c2e31f759252134c19cccb
  • Pointer size: 131 Bytes
  • Size of remote file: 892 kB
img/img28.png ADDED

Git LFS Details

  • SHA256: df0b48c5c80cbaf146035a17938e02ffd5485a3ba6539afff991d3ca98c4dcd3
  • Pointer size: 131 Bytes
  • Size of remote file: 896 kB
img/img3.png ADDED

Git LFS Details

  • SHA256: 01f70918c632000da522ccd0a2300a00e43e76882fa4618295d2c5d18aadb098
  • Pointer size: 131 Bytes
  • Size of remote file: 772 kB
img/img4.png ADDED

Git LFS Details

  • SHA256: abc9d861f05323abde7bfa7cbe15e18e125ce9c7e0440ab2a432e75a5a18bd6e
  • Pointer size: 131 Bytes
  • Size of remote file: 815 kB
img/img5.png ADDED

Git LFS Details

  • SHA256: b6c58f5551317fe8575f0031016193aa96693343b1d4f7c6a040c0e2342db09c
  • Pointer size: 131 Bytes
  • Size of remote file: 836 kB
img/img6.png ADDED

Git LFS Details

  • SHA256: 897efdd80532c7f453210c46d8475e8a3555db763948769c91a7b1c51cdf243c
  • Pointer size: 131 Bytes
  • Size of remote file: 769 kB
img/img7.png ADDED

Git LFS Details

  • SHA256: a6f32f14ad117c084253fe33eeaa3352fbf13a243c5e2801938150c1073da016
  • Pointer size: 131 Bytes
  • Size of remote file: 634 kB
img/img8.png ADDED

Git LFS Details

  • SHA256: 0ed478b8d0ac4e000e17e2f2260a7f5fe378fcbcdf14439ffcfb2c42983cd052
  • Pointer size: 131 Bytes
  • Size of remote file: 795 kB
img/img9.png ADDED

Git LFS Details

  • SHA256: f1087ba5023c581f57a3959edec9c62683cf47157608bcb26068ca20f9c65b47
  • Pointer size: 131 Bytes
  • Size of remote file: 761 kB
img_classification.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Space Deployment Process - Image Classification
2
+
3
+ ## Phase 1: Space Creation and Initial Setup
4
+ - **img15.png**: Hardware selection dialog (CPU basic, GPU options with pricing)
5
+ - **img16.png**: Initial Space creation form with SDK selection (Gradio)
6
+ - **img17.png**: Complete Space creation form (Janus Bot, gpl-3.0 license)
7
+ - **img18.png & img19.png**: HuggingFace dashboard with "New Space" option
8
+ - **img20.png**: HuggingFace dashboard with trending spaces
9
+
10
+ ## Phase 2: File Upload and Repository Management
11
+ - **img8.png**: Repository file view with uploaded files (.gitattributes, app.py, config.json, requirements.txt)
12
+ - **img9.png**: File upload interface with drag-and-drop (4 files selected)
13
+ - **img10.png**: macOS file picker with janus_bot folder files
14
+ - **img11.png**: Expanded macOS file picker with additional folders
15
+ - **img12.png**: Repository view with "Contribute" dropdown menu
16
+ - **img23.png**: File upload interface with commit options
17
+ - **img24.png**: macOS desktop with file upload process
18
+
19
+ ## Phase 3: Variables and Secrets Configuration
20
+ - **img3.png**: Secret creation dialog for "OPENROUTER_API_KEY"
21
+ - **img4.png**: Settings page Variables and secrets section (empty state)
22
+
23
+ ## Phase 4: Build Process and Status Monitoring
24
+ - **img7.png**: Build logs showing "Building..." with startup timestamp
25
+ - **img21.png**: Build queued status with commit SHA 3c89b48
26
+ - **img22.png**: Building notification banner
27
+
28
+ ## Phase 5: Configuration Verification and Testing
29
+ - **img1.png**: Working chatbot with successful configuration (green checkmarks)
30
+ - **img2.png**: Chat interface with configuration status and conversation starters
31
+ - **img5.png**: Space dropdown menu with API Key not configured warning
32
+ - **img6.png**: Chat interface showing API key configuration error
33
+ - **img25.png**: Repository view showing "Running" status
34
+ - **img26.png**: Final repository view with all files uploaded
35
+
36
+ ## Phase 6: Post-Deployment Documentation
37
+ - **img13.png**: Getting started guide with git clone instructions
38
+ - **img14.png**: Additional getting started instructions with app.py creation
39
+
40
+ ## Key Deployment Sequence
41
+ 1. **Create Space** (img15-20) β†’ **Upload Files** (img8-12, 23-24) β†’ **Configure Secrets** (img3-4) β†’ **Monitor Build** (img7, 21-22) β†’ **Verify Configuration** (img1-2, 5-6) β†’ **Final Testing** (img25-26)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio==5.37.0
2
+ requests>=2.32.3
3
+ beautifulsoup4>=4.12.3
4
+ python-dotenv>=1.0.0
5
+ uvicorn>=0.14.0
support_docs.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Support documentation module with accordion-style help sections
3
+ """
4
+
5
+ import gradio as gr
6
+ from datetime import datetime
7
+
8
+
9
+ def create_support_docs():
10
+ """Create the support documentation interface with accordion menus"""
11
+
12
+ with gr.Column():
13
+ gr.Markdown("# Support Documentation")
14
+ gr.Markdown("Step-by-step guidance for creating and deploying chat interfaces with HuggingFace Spaces.")
15
+
16
+ with gr.Accordion("πŸš€ Getting Started", open=True):
17
+ gr.Markdown("""
18
+ # Quick Start
19
+
20
+ Build a functional assistant with URL grounding.
21
+
22
+ **Steps:**
23
+ 1. Configure space settings and system prompt
24
+ 2. Enable URL grounding tools
25
+ 3. Preview and test
26
+ 4. Generate deployment package
27
+ 5. Deploy to HuggingFace
28
+
29
+ **Requirements:**
30
+ - HuggingFace account (free)
31
+ - OpenRouter API key
32
+ - Basic AI chatbot knowledge
33
+ """)
34
+
35
+ with gr.Accordion("βš™οΈ Configuration Guide", open=False):
36
+ gr.Markdown("""
37
+ ### Essential Settings
38
+
39
+ **Space Title & Description** - Name and purpose for your assistant
40
+
41
+ **Model Selection:**
42
+ - **Gemini 2.0 Flash** - Fast, reliable (recommended)
43
+ - **Gemma 3 27B** - Open-source, cost-effective
44
+ - **Claude 3.5 Haiku** - Advanced reasoning
45
+
46
+ **System Prompt & Templates:**
47
+ - **Custom** - Write your own specific instructions
48
+ - **Research Template** - Academic work, citations, fact-checking
49
+ - **Socratic Template** - Teaching through questions, critical thinking
50
+
51
+ **URL Grounding (up to 10 URLs)** - Static content for consistent context
52
+
53
+ **URL Priority System:**
54
+ - **Primary URLs (2)**: Main references (syllabus, textbook) - always processed first with highest priority
55
+ - **Secondary URLs (8)**: Supplementary sources - additional context as needed
56
+
57
+ The system processes URLs in order of priority, ensuring your most important content is always included in the assistant's context.
58
+
59
+ **Example Prompts Guidelines:**
60
+ - One prompt per line, under 100 characters
61
+ - Show assistant capabilities
62
+ - Examples: "Explain [topic] in simple terms", "Help prepare for exam", "Analyze this source: [URL]"
63
+
64
+ **Advanced Parameters:**
65
+ - **Temperature (0.0-2.0)**: 0.0-0.3 (focused), 0.4-0.7 (balanced), 0.8+ (creative)
66
+ - **Max Tokens (50-4096)**: 750 recommended for balanced responses
67
+ - **API Key Variable**: Default `OPENROUTER_API_KEY`
68
+ - **Access Code**: Optional student protection
69
+ """)
70
+
71
+ with gr.Accordion("πŸ”¬ Preview & Testing", open=False):
72
+ gr.Markdown("""
73
+ ### Pre-Deployment Testing
74
+
75
+ **Preview Steps:**
76
+ 1. Complete Configuration tab setup
77
+ 2. Click "Preview Deployment Package"
78
+ 3. Test chat interface and URL grounding
79
+ 4. Export conversations for documentation
80
+
81
+ **Requirements:** Set `OPENROUTER_API_KEY` environment variable for full testing
82
+
83
+ **Test Checklist:**
84
+ - Various query types and assistant responses
85
+ - URL grounding functionality (if enabled)
86
+ - System prompt behavior matches expectations
87
+ - Example prompts work correctly
88
+ """)
89
+
90
+ with gr.Accordion("πŸš€ HuggingFace Space Deployment", open=False):
91
+ gr.Markdown("""
92
+ ### Complete Deployment Workflow
93
+
94
+ **Overview:** Generate Package β†’ Create Space β†’ Upload Files β†’ Configure Secrets β†’ Monitor Build β†’ Verify Configuration
95
+ """)
96
+
97
+ with gr.Accordion("Step 1: Generate & Create Space", open=False):
98
+ gr.Markdown("""
99
+ **1.1 Generate Deployment Package**
100
+ - Click "Generate Deployment Package" and download zip
101
+ - Extract files: `app.py`, `config.json`, `requirements.txt`
102
+
103
+ **1.2 Create New Space**
104
+ - Go to [huggingface.co/spaces](https://huggingface.co/spaces)
105
+ - Click "New Space"
106
+ - Choose Gradio SDK
107
+ - Select hardware (CPU Basic is free)
108
+ """)
109
+
110
+ with gr.Row():
111
+ with gr.Column(scale=1):
112
+ gr.Image(
113
+ value="img/img17.png",
114
+ label="Space Creation Form",
115
+ show_label=True,
116
+ interactive=False,
117
+ width=400,
118
+ container=False
119
+ )
120
+ with gr.Column(scale=1):
121
+ gr.Image(
122
+ value="img/img16.png",
123
+ label="Hardware Selection",
124
+ show_label=True,
125
+ interactive=False,
126
+ width=400,
127
+ container=False
128
+ )
129
+
130
+ with gr.Accordion("Step 2: Upload Files", open=False):
131
+ gr.Markdown("""
132
+ **2.1 Upload Project Files**
133
+ - Navigate to Files tab
134
+ - Click "Upload files" or drag and drop
135
+ - Upload: `app.py`, `config.json`, `requirements.txt`
136
+ - Commit directly to main branch
137
+ """)
138
+
139
+ with gr.Row():
140
+ with gr.Column(scale=1):
141
+ gr.Image(
142
+ value="img/img12.png",
143
+ label="File Upload",
144
+ show_label=True,
145
+ interactive=False,
146
+ width=400,
147
+ container=False
148
+ )
149
+ with gr.Column(scale=1):
150
+ gr.Image(
151
+ value="img/img8.png",
152
+ label="Files After Upload",
153
+ show_label=True,
154
+ interactive=False,
155
+ width=400,
156
+ container=False
157
+ )
158
+
159
+ with gr.Accordion("Step 3: Configure API Secrets", open=False):
160
+ gr.Markdown("""
161
+ **3.1 Configure HuggingFace Spaces Secrets**
162
+ - Go to Settings β†’ Variables and secrets
163
+ - Click "New secret"
164
+
165
+ **Required Secret:**
166
+ - **OPENROUTER_API_KEY**: Set this to your OpenRouter API key value
167
+
168
+ **Optional Secret:**
169
+ - **SPACE_ACCESS_CODE**: Set this to your desired access code value for student access control
170
+
171
+ **Access Control Notes:**
172
+ - If you don't create the `SPACE_ACCESS_CODE` secret, your Space will be publicly accessible
173
+ - To enable access control, create the `SPACE_ACCESS_CODE` secret with your chosen code value
174
+ - To disable access control, delete the `SPACE_ACCESS_CODE` secret entirely
175
+ - Do NOT set an empty value - either set a code or don't create the secret at all
176
+ """)
177
+
178
+ with gr.Row():
179
+
180
+ with gr.Column(scale=1):
181
+ gr.Image(
182
+ value="img/img4.png",
183
+ label="Navigating to Settings",
184
+ show_label=True,
185
+ interactive=False,
186
+ width=400,
187
+ container=False
188
+ )
189
+
190
+ with gr.Column(scale=1):
191
+ gr.Image(
192
+ value="img/img4.png",
193
+ label="Settings Variables and Secrets",
194
+ show_label=True,
195
+ interactive=False,
196
+ width=400,
197
+ container=False
198
+ )
199
+
200
+ with gr.Column(scale=1):
201
+ gr.Image(
202
+ value="img/img3.png",
203
+ label="API Key Secret Configuration",
204
+ show_label=True,
205
+ interactive=False,
206
+ width=400,
207
+ container=False
208
+ )
209
+
210
+ with gr.Accordion("Step 4: Monitor Build & Verify Configuration", open=False):
211
+ gr.Markdown("""
212
+ **4.1 Build Monitoring**
213
+ - Space will show "Building..." status
214
+ - Monitor build logs for errors
215
+ - Wait 1-3 minutes for completion
216
+
217
+ **4.2 Configuration Verification**
218
+ - Check Configuration Status panel
219
+ - API Key should show "Configured and valid" βœ…
220
+ - Test chat interface with example prompts
221
+ """)
222
+
223
+ with gr.Row():
224
+ with gr.Column(scale=1):
225
+ gr.Image(
226
+ value="img/img7.png",
227
+ label="Build Process",
228
+ show_label=True,
229
+ interactive=False,
230
+ width=400,
231
+ container=False
232
+ )
233
+ with gr.Column(scale=1):
234
+ gr.Image(
235
+ value="img/img1.png",
236
+ label="Successful Configuration",
237
+ show_label=True,
238
+ interactive=False,
239
+ width=400,
240
+ container=False
241
+ )
242
+
243
+ gr.Markdown("""
244
+ **Configuration Status Indicators:**
245
+ - ❌ Red X: API key not configured or invalid
246
+ - βœ… Green check: All settings configured correctly
247
+ - πŸ”„ Building: Space is updating with new changes
248
+ """)
249
+
250
+ with gr.Accordion("πŸ”§ Troubleshooting", open=False):
251
+ gr.Markdown("""
252
+ ### Common Issues and Solutions
253
+
254
+ **Configuration Status Shows Red X:**
255
+ - Verify API key secret name matches configuration (usually `OPENROUTER_API_KEY`)
256
+ - Check OpenRouter account has credits
257
+ - Regenerate API key if needed
258
+
259
+ **Build Failed:**
260
+ - Check `requirements.txt` formatting (no extra spaces)
261
+ - Try reuploading files if build gets stuck
262
+ - Monitor build logs for specific errors
263
+
264
+ **API Errors:**
265
+ - **401 Unauthorized**: Invalid API key or incorrect secret name
266
+ - **429 Rate Limited**: Wait a few minutes, consider upgrading OpenRouter plan
267
+
268
+ **Access Issues:**
269
+ - **Access code not working**: Verify `SPACE_ACCESS_CODE` secret is set correctly (case-sensitive)
270
+ - **URLs not fetching**: Check URLs are publicly accessible, some sites block automated requests
271
+ """)
272
+
273
+ with gr.Accordion("πŸ“š Additional Resources", open=False):
274
+ gr.Markdown("""
275
+ ### Helpful Links
276
+
277
+ **HuggingFace Documentation**
278
+ - [Spaces Overview](https://huggingface.co/docs/hub/spaces-overview)
279
+ - [Gradio on Spaces](https://huggingface.co/docs/hub/spaces-gradio)
280
+ - [Environment Variables](https://huggingface.co/docs/hub/spaces-overview#managing-secrets)
281
+
282
+ **OpenRouter Documentation**
283
+ - [API Keys](https://openrouter.ai/keys)
284
+ - [Model Comparison](https://openrouter.ai/models)
285
+ - [Pricing](https://openrouter.ai/docs#models)
286
+
287
+ **Gradio Documentation**
288
+ - [Chat Interface](https://gradio.app/docs/chatinterface)
289
+ - [Components](https://gradio.app/docs/)
290
+ - [Sharing Apps](https://gradio.app/sharing-your-app/)
291
+
292
+ **Community Support**
293
+ - [HuggingFace Community Forums](https://discuss.huggingface.co/)
294
+ - [Gradio Discord](https://discord.gg/feTf9x3ZSB)
295
+
296
+ **Educational Use Cases**
297
+ - Course assistants for Q&A
298
+ - Research writing support
299
+ - Study guide generation
300
+ - Document analysis tools
301
+ - Language practice partners
302
+ """)
303
+
304
+ def export_conversation_to_markdown(conversation_history, config_metadata=None):
305
+ """Export conversation history to markdown format with configuration metadata"""
306
+ if not conversation_history:
307
+ return "No conversation to export."
308
+
309
+ markdown_content = f"""# Conversation Export
310
+ Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
311
+
312
+ """
313
+
314
+ # Add configuration metadata if provided
315
+ if config_metadata:
316
+ markdown_content += """## Configuration Information
317
+
318
+ """
319
+
320
+ # Add basic configuration details
321
+ if config_metadata.get('name'):
322
+ markdown_content += f"**Assistant Name:** {config_metadata['name']}\n"
323
+ if config_metadata.get('description'):
324
+ markdown_content += f"**Description:** {config_metadata['description']}\n"
325
+ if config_metadata.get('model'):
326
+ markdown_content += f"**Model:** {config_metadata['model']}\n"
327
+ if config_metadata.get('temperature'):
328
+ markdown_content += f"**Temperature:** {config_metadata['temperature']}\n"
329
+ if config_metadata.get('max_tokens'):
330
+ markdown_content += f"**Max Tokens:** {config_metadata['max_tokens']}\n"
331
+
332
+ # Add URL grounding information
333
+ grounding_urls = []
334
+ for i in range(1, 5):
335
+ url = config_metadata.get(f'url{i}')
336
+ if url and url.strip():
337
+ grounding_urls.append(url.strip())
338
+
339
+ if grounding_urls:
340
+ markdown_content += f"\n**URL Grounding ({len(grounding_urls)} URLs):**\n"
341
+ for i, url in enumerate(grounding_urls, 1):
342
+ markdown_content += f"- URL {i}: {url}\n"
343
+
344
+ # Add feature flags
345
+ if config_metadata.get('enable_dynamic_urls'):
346
+ markdown_content += f"\n**Dynamic URL Fetching:** Enabled\n"
347
+
348
+ # Add system prompt
349
+ if config_metadata.get('system_prompt'):
350
+ system_prompt = config_metadata['system_prompt']
351
+ markdown_content += f"\n**System Prompt:**\n```\n{system_prompt}\n```\n"
352
+
353
+ markdown_content += "\n---\n\n"
354
+ else:
355
+ markdown_content += "---\n\n"
356
+
357
+ for i, message in enumerate(conversation_history):
358
+ if isinstance(message, dict):
359
+ role = message.get('role', 'unknown')
360
+ content = message.get('content', '')
361
+
362
+ if role == 'user':
363
+ markdown_content += f"## User Message {(i//2) + 1}\n\n{content}\n\n"
364
+ elif role == 'assistant':
365
+ markdown_content += f"## Assistant Response {(i//2) + 1}\n\n{content}\n\n---\n\n"
366
+
367
+ return markdown_content